@thincoder/core 0.10.12 → 0.11.1
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 +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
|
@@ -24,7 +24,7 @@ import { tokenExpired, removeDesignTokenSlot, reconcileEngTokensFromSlot } from
|
|
|
24
24
|
import { resolveChildProvider, buildChildRunOpts, enqueueAsk } from "./subagent-async.mjs"
|
|
25
25
|
import { nextSubagentId } from "./subagent-scheduler.mjs"
|
|
26
26
|
import {
|
|
27
|
-
normalizeFileList, describeBlockers, assertNoDepCycle, depInfo,
|
|
27
|
+
normalizeFileList, describeBlockers, assertNoDepCycle, depInfo, batchDocInFlight,
|
|
28
28
|
} from "./subagent-scheduler.mjs"
|
|
29
29
|
// M5 F2(ENGINEERING-MODE-V2-MODULE-DELEGATION §2.3):任务书 round 枚举 + 五项结构化参数校验/拼装本体落
|
|
30
30
|
// spawn-gates.mjs(纯谓词零依赖)——本处只加 import 调用。
|
|
@@ -37,7 +37,21 @@ import {
|
|
|
37
37
|
import { buildAuditBlock } from "./audit-block.mjs"
|
|
38
38
|
// 批次档路径解析单源(BATCH-RECORD.md §4.15 · 台账 #287):门内解析改经叶档(读面非抛形),
|
|
39
39
|
// 与 append / create 同源——门判据仍「参数在 + 路径可读」,错误文案逐字零变。
|
|
40
|
-
import { resolveBatchReadPath } from "./batch-paths.mjs"
|
|
40
|
+
import { resolveBatchReadPath, resolveBatchReadResult } from "./batch-paths.mjs"
|
|
41
|
+
// `files` 落点单源 = 统一解析层(实调点 = `normalizeFileList`——`thincoder-core/agent-tools/subagent-scheduler.mjs`
|
|
42
|
+
// 的 `const abs = resolvePathTarget(cwd, f, ...)`):本档不再预解析(预解析会让该面的三条门判吃绝对形而漏判)。
|
|
43
|
+
import { danglingSameNameHint } from "../path-resolve.mjs"
|
|
44
|
+
|
|
45
|
+
/** 不可读拒文的追加提示(§2.6:既有文案逐字在前)——同名候选 ∥ 判不出唯一的候选全列;无提示 ⇒ 空串。 */
|
|
46
|
+
function unreadableHint(cwd, given) {
|
|
47
|
+
const r = resolveBatchReadResult(cwd, given)
|
|
48
|
+
if (r.state === "dangling") {
|
|
49
|
+
const hint = danglingSameNameHint(r)
|
|
50
|
+
return hint ? `;${hint}` : ""
|
|
51
|
+
}
|
|
52
|
+
if (r.state === "ambiguous") return `;Candidates: ${r.candidates.join(" ∥ ")}`
|
|
53
|
+
return ""
|
|
54
|
+
}
|
|
41
55
|
|
|
42
56
|
/** 任务书播报文本:该角色名集拼装 + 可选 task 自由文本(拼装空 ⇒ 原 task 逐字——旧形零变)。 */
|
|
43
57
|
function taskBriefOf(args, role) {
|
|
@@ -167,6 +181,16 @@ export function prepareScheduling(parent, filesRaw, dependsRaw, wantAsync) {
|
|
|
167
181
|
return { files, dependsOn, errorJson: null }
|
|
168
182
|
}
|
|
169
183
|
|
|
184
|
+
/** S4 写域行(AGENT-LOOP-SUBAGENT.md §6.26「写域行(S4)」):表头 + 逐条绝对路径 + 两句语义——
|
|
185
|
+
* 本档局部纯函数(D23-9:不外提模块)。两句不可省:缺① 子代理读成「我只能写这些」⇒ 该写的漏掉;
|
|
186
|
+
* 缺② 仍把任务书 §2 全文当成自己的改动面(本块的病因)。清单不截断——截断正是本块要消除的
|
|
187
|
+
* 「看不见」。返回块文本(块间分隔由装配侧 `join("\n\n")` 施加)。 */
|
|
188
|
+
function filesBlockOf(files) {
|
|
189
|
+
return `Files declared for this spawn (the write domain this task declares, for scheduling):\n` +
|
|
190
|
+
files.map((f) => `- ${f}`).join("\n") +
|
|
191
|
+
`\n\nThis list is not an authorization boundary: touching a file outside it is allowed — report every out-of-list file you changed, with its reason. It is also not the whole change surface: other spawns on this task declare their own files, and what is listed here is your share only.`
|
|
192
|
+
}
|
|
193
|
+
|
|
170
194
|
/**
|
|
171
195
|
* AGENT-LOOP-SUBAGENT.md §6.9 准入通过后的 child 装配(2026-09-05 module-split——自 execute 参数化提取,
|
|
172
196
|
* 原 318-488 段 verbatim——语义零变)。副作用保留:relay 取号(sync 支 allocRelay——
|
|
@@ -194,25 +218,44 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
194
218
|
let batchDocAbs = null
|
|
195
219
|
if (engineeringRole) {
|
|
196
220
|
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
221
|
+
// 「无果 ⇒ 既有文案 + 追加同名候选」(§2.6——既有拒文逐字在前,层的提示只追加)。
|
|
197
222
|
const refusal = (suffix = "") => new Error(
|
|
198
223
|
`batchDoc is required for role='${role}' — pass the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute); spawn refused without it.` + suffix)
|
|
199
224
|
if (!given) throw refusal()
|
|
200
|
-
batchDocAbs = resolveBatchReadPath(parent.cwd
|
|
201
|
-
if (!batchDocAbs) throw refusal(
|
|
225
|
+
batchDocAbs = resolveBatchReadPath(parent.cwd, given)
|
|
226
|
+
if (!batchDocAbs) throw refusal(` (given path is not a readable file${unreadableHint(parent.cwd, given)})`)
|
|
202
227
|
} else if (role === "coder") {
|
|
203
228
|
const given = typeof args.batchDoc === "string" ? args.batchDoc.trim() : ""
|
|
204
229
|
if (given) {
|
|
205
|
-
batchDocAbs = resolveBatchReadPath(parent.cwd
|
|
230
|
+
batchDocAbs = resolveBatchReadPath(parent.cwd, given)
|
|
206
231
|
if (!batchDocAbs) {
|
|
207
|
-
throw new Error("batchDoc for role='coder' — pass the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute); when provided it must resolve to a readable file (omit batchDoc to spawn without the §5 record-write binding).")
|
|
232
|
+
throw new Error("batchDoc for role='coder' — pass the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute); when provided it must resolve to a readable file (omit batchDoc to spawn without the §5 record-write binding)." + unreadableHint(parent.cwd, given))
|
|
208
233
|
}
|
|
209
234
|
}
|
|
210
235
|
}
|
|
211
236
|
|
|
237
|
+
// ── AGENT-LOOP-SUBAGENT.md §6.58 批次档复用护栏(2026-10-28 batchdoc-reuse-guard)──
|
|
238
|
+
// 同一档已有在飞 spawn ⇒ 拒发(2026-10-28 11:34 事故:两席共用一本批档、文件域互不重叠 ⇒
|
|
239
|
+
// 语法坏档 + 一席复原)。判据 = 池内在飞条目的 `_batchDoc` 与本次解析出的绝对路径按 fileKey
|
|
240
|
+
// 相等(谓词单源 = `batchDocInFlight`——池派生事实住 subagent-scheduler.mjs,读法同 `depInfo`)。
|
|
241
|
+
// 只用结构性事实(池 + 路径相等),不做文本匹配(TOOLS.md §6.4)。
|
|
242
|
+
// **不读 `round`**(KD-BG1):修正轮靠「前一轮已 settle ⇒ 不在在飞集」自然放行;前一轮仍
|
|
243
|
+
// 在飞就派修正轮 = 同一种两写者病,照拒。
|
|
244
|
+
// **非对称是判据的一部分,勿「顺手统一」**:被检面只覆盖工程角色(本分支内),占用者是池内
|
|
245
|
+
// 任何带 `_batchDoc` 的条目、不分角色(普通面 coder 携 batchDoc 也算);sync spawn 不入池 ⇒
|
|
246
|
+
// 它不是占用者(§6.58.7 第 7 条)。
|
|
247
|
+
// 拒因文案逐字模板 = §6.58.2(文案单源 = 本处——下游断言逐字对照)。
|
|
248
|
+
if (engineeringRole) {
|
|
249
|
+
const occupant = batchDocInFlight(parent, batchDocAbs)
|
|
250
|
+
if (occupant) {
|
|
251
|
+
throw new Error(`batchDoc is already in flight — spawn refused. Batch record: ${batchDocAbs}. Occupant: ${occupant.role}#${occupant.id} (status: ${occupant.status}). One batch record serves one concurrent spawn: wait for ${occupant.role}#${occupant.id} to settle, or cancel it, then spawn again.`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
212
255
|
// M5 F2 + §6.22(2026-10-24)任务书门——按角色分流:
|
|
213
256
|
// 工程角色(eng-coder / eng-designer):零任务书文本参数——八个自由文本键命中即拒(键在场即拒)并点名
|
|
214
257
|
// (值全空白也拒);随后 round 枚举(initial|fix)。
|
|
215
|
-
// explore / plan / coder:四项核心 + `scope` 必带(判据三条零改:存在 ∧ string ∧ trim 非空)。
|
|
258
|
+
// explore / plan / coder / tester:四项核心 + `scope` 必带(判据三条零改:存在 ∧ string ∧ trim 非空)。
|
|
216
259
|
// 落点 = batchDoc 门之后、token 门之前(门序 = AGENT-LOOP-SUBAGENT.md §6.22.1)。
|
|
217
260
|
if (engineeringRole) {
|
|
218
261
|
rejectEngFreeTextParams(args, role)
|
|
@@ -232,6 +275,19 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
232
275
|
if (role === "explore" || role === "plan") {
|
|
233
276
|
const allowed = readonlyToolNames(parent.tools)
|
|
234
277
|
tools = parent.tools.filter((t) => allowed.has(t.name))
|
|
278
|
+
} else if (role === "tester") {
|
|
279
|
+
// tester = the THIRD tool face (AGENT-LOOP-SUBAGENT.md §6.59.2 ④): read-only set ∪ {execute, bash},
|
|
280
|
+
// minus {verify, advisor}.
|
|
281
|
+
// · Allowlist form (D-TT1) — a new non-readonly tool stays OUT by default (the write face is fail-closed);
|
|
282
|
+
// a new readonly tool joins automatically through the same formula.
|
|
283
|
+
// · Why subtract `verify` / `advisor`: both produce a VERDICT, not a reading — "I declare this delivery
|
|
284
|
+
// good" and "open a review, produce a judgment". A tester's product is readings (F-TT3: it does not
|
|
285
|
+
// close a judgment on the parent's behalf), and `advisor` is `readonly: true`, so leaving it in would
|
|
286
|
+
// make 「it does not run reviews」 a sentence nobody enforces. Both subtractions are by name, explicit,
|
|
287
|
+
// and do not depend on the tool's `readonly` flag — that flag says "writes nothing", not "concludes nothing".
|
|
288
|
+
const allowed = readonlyToolNames(parent.tools)
|
|
289
|
+
const TRIMMED = new Set(["verify", "advisor"])
|
|
290
|
+
tools = parent.tools.filter((t) => (allowed.has(t.name) && !TRIMMED.has(t.name)) || t.name === "execute" || t.name === "bash")
|
|
235
291
|
} else {
|
|
236
292
|
tools = parent.tools
|
|
237
293
|
}
|
|
@@ -331,8 +387,8 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
331
387
|
// 两处(批次档路径行 + 审计模板块)原随 user 首条下发,压缩把首条并入摘要即丢(长任务中后期
|
|
332
388
|
// 既看不到批次档路径、也看不到审计模板 ⇒ 过度勘察的真病根);固块值在 child 生命周期内恒定
|
|
333
389
|
// ⇒ run 内 system 前缀逐字节稳定(前缀缓存不破),且不在 `agent.history` 内 ⇒ 压缩结构上吞不掉。
|
|
334
|
-
// 收集序 =
|
|
335
|
-
// 的 system 逐字节同改前——§6.26 缓存契约兼容证明)。
|
|
390
|
+
// 收集序 = 批次档行 → 审计块 → 写域行(S4,见下);空数组 ⇒ 不绑定(depth-0 / explore / plan /
|
|
391
|
+
// consult 的 system 逐字节同改前——§6.26 缓存契约兼容证明)。
|
|
336
392
|
const spawnBlocks = []
|
|
337
393
|
// §2.11 第 1 点(FR16 载体 + FR17 铁律 #5 的机械面):批次档**绝对路径**下发给工程角色
|
|
338
394
|
// (eng-coder 实现 / eng-designer 写稿)——任务书本体 = 本档 §2;本行让子代理"拿到本档路径"。
|
|
@@ -342,6 +398,9 @@ export function buildSpawnChild(parent, ctx, args, role, wantAsync, files, depen
|
|
|
342
398
|
// `_touchedFiles` ∪ 批档 §2(审计子代理自读)——**不是** eng-coder 自述(自述可恰好漏掉应抓的
|
|
343
399
|
// 越界档)。块文本(含模板逐字)住 `./audit-block.mjs`(D23-3 外提)。
|
|
344
400
|
if (engAuditAttempt !== null) spawnBlocks.push(buildAuditBlock(ctx))
|
|
401
|
+
// §6.26「写域行(S4)」:`files`(本 spawn 的预期写域,`prepareScheduling` 已归一化)非空 ⇒ 追加
|
|
402
|
+
// 一格——写域此前只有调度器知道,子代理只能猜自己的改动面;空 / 未传 ⇒ 不推块,system 逐字节同改前。
|
|
403
|
+
if (files.length > 0) spawnBlocks.push(filesBlockOf(files))
|
|
345
404
|
// The child's own task input rides the child object: D23-6 —— `_engTaskInput` 语义 = **一行机械
|
|
346
405
|
// 任务书行**(批档绝对路径 + §2 指向句;不含固块)。
|
|
347
406
|
if (engineeringRole) child._engTaskInput = input
|
package/agent-tools/subagent.mjs
CHANGED
|
@@ -121,7 +121,9 @@ export function buildSyncStoppedReport(role, capturedOutput, designId) {
|
|
|
121
121
|
* (suitable for codebase exploration); "coder" — full tool set, self-contained
|
|
122
122
|
* implementation tasks; "plan" — read-only planning; "eng-coder" —
|
|
123
123
|
* engineering-mode implementation (design-token gated); "eng-designer" —
|
|
124
|
-
* engineering-mode design writing (requirements + design docs, batchDoc gated)
|
|
124
|
+
* engineering-mode design writing (requirements + design docs, batchDoc gated);
|
|
125
|
+
* "tester" — verification runs and readings (read-only set ∪ execute/bash; no write tool in the table —
|
|
126
|
+
* run-only is the role contract, not a sandbox).
|
|
125
127
|
* - no role specified — invalid by design since the 2026-08-25 fail-closed gate
|
|
126
128
|
* (role is mandatory; "no role → same tool set as parent" was removed with the
|
|
127
129
|
* coder-leak fix and the header text above predates it)
|
|
@@ -139,22 +141,22 @@ export const subagentTool = {
|
|
|
139
141
|
action: { type: "string", enum: ["spawn", "status", "escalate", "cancel", "panel", "consume-design", "observe", "send", "design-slots"], description: "Which action — spawn (default) / status / observe / send / escalate / cancel / panel / consume-design / design-slots; semantics per action in the tool description." },
|
|
140
142
|
view: { type: "boolean", description: "action:'panel' only: true (default) = the live panel blocks as the user sees them; false without freeze = error. Mutually exclusive with freeze (freeze wins)." },
|
|
141
143
|
freeze: { type: "string", description: "action:'panel' only: block key of a digested-stuck block — reclaims it into the conversation; refused for running/done/unknown blocks or a still-pending report; works on the CLI TUI panel and on the desktop renderer panel (live readback of the blocks as the user sees them)." },
|
|
142
|
-
task: { type: "string", description: "For action:'spawn':
|
|
143
|
-
context: { type: "string", description: "Optional background for
|
|
144
|
-
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
144
|
+
task: { type: "string", description: "For action:'spawn': non-engineering roles (explore / plan / coder / tester) — OPTIONAL free-text supplement; the brief itself is the four core parameters (goal / knownFacts / acceptance / reportFormat) + scope. Engineering roles (eng-coder / eng-designer) take NO task-book text parameters — their task book is batch record §2 (batchDoc), read by the child itself. For action:'escalate': the brief (goal, constraints, entry files, acceptance criteria)." },
|
|
145
|
+
context: { type: "string", description: "Optional background for non-engineering spawns (explore / plan / coder / tester) only; action:'spawn' only." },
|
|
146
|
+
role: { type: "string", enum: ["explore", "plan", "coder", "eng-coder", "eng-designer", "tester"], description: "The sub-agent role — see the role matrix in the tool description. Exact spelling required. action:'spawn' only (escalate spawns its own expert internally)." },
|
|
145
147
|
model: { type: "string", description: "action:'spawn': provider/model override ('provider:model', a provider, or a model name on the parent's provider) — defaults config.agent.subagentModels[role] → config.agent.subagentModel → the parent's provider; \"default\" inherits. action:'escalate': a consult candidate (default = the first)." },
|
|
146
148
|
designToken: { type: "string", description: "REQUIRED for role='eng-coder': the exact token issued by advisor(type='design') after the review passed — copy it verbatim from the design-slots row (the text after designToken=). missing or mismatched = the spawn is refused; an expired token is refused with expired named. Pass it on every eng-coder spawn, fix rounds included (they reuse the same token)." },
|
|
147
149
|
designId: { type: "string", description: "role='eng-coder': the designId echoed with the approved token — required to pick between concurrent designs (optional for one). action:'consume-design': the single-slot selector (required to pick between several; the gate refuses to guess); see designIds / expired / olderThanDays for batch consumption. action:'design-slots': filter the inventory to one slot (trimmed exact match; an empty string means no filter; an unknown id is refused with the held id list)." },
|
|
148
150
|
designIds: { type: "array", items: { type: "string" }, description: "action:'consume-design' (batch): an explicit non-empty list of design ids to close out — pass exactly ONE selector per call (designId / designIds / expired / olderThanDays)." },
|
|
149
151
|
expired: { type: "boolean", description: "action:'consume-design' (batch): true = close out every expired slot (exactly ONE selector per call)." },
|
|
150
152
|
olderThanDays: { type: "number", description: "action:'consume-design' (batch): close out every slot older than N days (age > N; exactly ONE selector per call)." },
|
|
151
|
-
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. This IS the task book for engineering roles — batch record §2, read by the child itself. Refused when absent
|
|
153
|
+
batchDoc: { type: "string", description: "REQUIRED for role='eng-coder'/'eng-designer': the batch record path (e.g. <repo>/docs/batches/<batch>-<topic>.md; relative paths resolve against the session cwd first, then candidate project roots — or absolute) — the task book this spawn implements/writes. This IS the task book for engineering roles — batch record §2, read by the child itself. Refused when absent, when the path does not resolve to a readable file, or when that record already has an in-flight spawn — one batch record serves one concurrent spawn: wait for the occupant to settle (or cancel it) before spawning again. The content is never validated. role='coder' (normal mode): optional — binds this spawn's §5 record-write channel (unbound ⇒ the main agent records §5 on its behalf); when provided the path must resolve to a readable file." },
|
|
152
154
|
round: { type: "string", enum: ROUND_VALUES, description: "REQUIRED for role='eng-coder'/'eng-designer': initial (first round for this design) or fix (a correction round reusing the same designId+designToken; docs FIRST). No default — missing = refused." },
|
|
153
|
-
goal: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (
|
|
154
|
-
knownFacts: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (
|
|
155
|
-
scope: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (
|
|
156
|
-
acceptance: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (
|
|
157
|
-
reportFormat: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder' (
|
|
155
|
+
goal: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder'/'tester' (non-engineering task book): the task book's goal & why — the problem this solves and the cost chain (content only, no label — the runtime composes the labels)." },
|
|
156
|
+
knownFacts: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder'/'tester' (non-engineering task book): the facts and file paths the parent already verified — the child must not re-explore them." },
|
|
157
|
+
scope: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder'/'tester' (non-engineering task book, the fifth parameter): scope & forbidden scope — what to read / search and what not to touch." },
|
|
158
|
+
acceptance: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder'/'tester' (non-engineering task book): acceptance criteria — machine-verifiable (commands / thresholds / assertion counts)." },
|
|
159
|
+
reportFormat: { type: "string", description: "REQUIRED for role='explore'/'plan'/'coder'/'tester' (non-engineering task book): the delivery-report format the child must produce." },
|
|
158
160
|
async: { type: "boolean", description: "action:'spawn'/'escalate': default at depth 0 = true (async; the report arrives automatically); depth>0 always sync. async:false forces the blocking flight." },
|
|
159
161
|
files: { type: "array", items: { type: "string" }, description: "action:'spawn' only: the file write-domain this task declares — file-level paths only (cwd-relative or absolute; directories rejected — they bypass the conflict detector). Overlapping-file tasks serialize automatically. Omit = no conflict detection." },
|
|
160
162
|
dependsOn: { type: "array", items: { type: "string" }, description: "action:'spawn' only: ids from prior async returns whose outcome this task needs — queues until every dependency settles, then starts. Consumed ids count as satisfied; a cancelled/failed dependency stays queued marked 'dependency cancelled' until you decide (AUTO sessions auto-start). Unknown ids error." },
|
|
@@ -225,7 +227,7 @@ export const subagentTool = {
|
|
|
225
227
|
// 工程角色 = 无任务书文本参数(任务书 = 批次档 §2):此处不判——自由文本键由
|
|
226
228
|
// buildSpawnChild 的任务书门键在场即拒并点名(空集拼装不得误报「缺任务书」)。
|
|
227
229
|
if (!isEngineeringRole(args.role) && !composeTaskBookBrief(args, args.role) && (typeof args.task !== "string" || !args.task.trim())) {
|
|
228
|
-
throw new Error("subagent action:'spawn' requires the structured task-book parameters for
|
|
230
|
+
throw new Error("subagent action:'spawn' requires the structured task-book parameters for non-engineering roles (explore / plan / coder / tester) (four core: goal / knownFacts / acceptance / reportFormat + scope) — a free-text task alone is not the brief. Engineering roles take no task-book text parameters: their task book is batch record §2.")
|
|
229
231
|
}
|
|
230
232
|
// §18 D-E1a depth-gated async default (2026-09-06 需求池 R12): depth-0 spawns
|
|
231
233
|
// default to async for EVERY role (the old role-level default — eng-coder only —
|
|
@@ -238,9 +240,9 @@ export const subagentTool = {
|
|
|
238
240
|
// variant roles ("Coder", " coder") bypass BOTH mode gates and fall through to
|
|
239
241
|
// full tools / no overlay — a full-write coder without design review. Schema enums are
|
|
240
242
|
// advisory; providers don't enforce them. Fail closed on unknown roles.
|
|
241
|
-
const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer"])
|
|
243
|
+
const ROLES = new Set(["explore", "plan", "coder", "eng-coder", "eng-designer", "tester"])
|
|
242
244
|
if (!ROLES.has(role)) {
|
|
243
|
-
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer (exact spelling).`)
|
|
245
|
+
throw new Error(`Unknown subagent role: ${JSON.stringify(role)}. Valid roles: explore, plan, coder, eng-coder, eng-designer, tester (exact spelling).`)
|
|
244
246
|
}
|
|
245
247
|
// AGENT-LOOP-SUBAGENT.md §6.7.6 D-E3 internal-spawn gate: an eng-coder sub-agent may only spawn sync
|
|
246
248
|
// explore (audit) children — non-explore roles and async are refused here
|
|
@@ -254,7 +256,7 @@ export const subagentTool = {
|
|
|
254
256
|
throw new Error("Engineering mode: role='coder' is disabled — use role='eng-coder' for implementation tasks (or role='eng-designer' for design writing).")
|
|
255
257
|
}
|
|
256
258
|
if (parent.config?.agent?.engineering && role === "plan") {
|
|
257
|
-
throw new Error("Engineering mode: role='plan' is disabled — the engineering-mode enum is explore / eng-designer / eng-coder (plan is a normal-mode role).")
|
|
259
|
+
throw new Error("Engineering mode: role='plan' is disabled — the engineering-mode enum is explore / eng-designer / eng-coder / tester (plan is a normal-mode role).")
|
|
258
260
|
}
|
|
259
261
|
if (!parent.config?.agent?.engineering && role === "eng-coder") {
|
|
260
262
|
throw new Error("Engineering mode is not active — use role='coder' for implementation tasks.")
|
package/agent.mjs
CHANGED
|
@@ -73,6 +73,7 @@ export function createAgent({
|
|
|
73
73
|
_pendingTimers: [],
|
|
74
74
|
_sessionStart: sessionStart,
|
|
75
75
|
_lastPromptTokens: null, _usageAtLen: null,
|
|
76
|
+
_tok: null, _tokRatio: 1, // §6.17 估算自校准池化比({key,num,den};采样 = turn-loop,清空 = /new)
|
|
76
77
|
_compressFailures: 0,
|
|
77
78
|
_emptyRetries: 0, // empty-response retry budget (per-run; reset on a fresh user turn)
|
|
78
79
|
_runStartHistoryLen: 0, // machine-line length at the start of the current run — end-of-run exploration distillation slices from here
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* client-identity.mjs — 客户端标识单源(产品名 ∥ 端类型 ∥ 版本)——PROVIDER.md §6.17
|
|
3
|
+
* 「客户端标识头(`User-Agent`)」。
|
|
4
|
+
*
|
|
5
|
+
* 核件**不猜自己在哪个端**:不读 `process.argv` ∥ 环境变量 ∥ 任何 `package.json`。四端(cli ∥ vscode ∥
|
|
6
|
+
* desktop ∥ server)各在进程内 `setClientIdentity` 一次(前三端 = 启动期求值;server = 核装载成功处——
|
|
7
|
+
* 无核即无模型请求);无人声明 ⇒ 哨兵 `core/0.0.0`(真实端版本恒不等于 `0.0.0`——四端进程内出现即声明漏了)。
|
|
8
|
+
*
|
|
9
|
+
* `withUserAgent` 是全部装配点的唯一入口:**先删任何大小写变体的 `user-agent` 键**,再写本产品值——
|
|
10
|
+
* 渠道配置 `providers[].headers` 因此盖不掉它(`fetch` 的 `Headers` 对大小写变体同名键是合并语义,
|
|
11
|
+
* 只靠「后写覆盖」会得 `evil, ThinCoder/…` 双值)。
|
|
12
|
+
*
|
|
13
|
+
* 同一入口也是定制头值位模板 `${sessionId}` 的解析单源(PROVIDER.md §6.17「会话标识头」):
|
|
14
|
+
* **每次请求现取** `getSessionId()`,值不冻结进 provider 对象;只有声明该模板的渠道才发对应的头。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { getSessionId } from "./session-identity.mjs"
|
|
18
|
+
|
|
19
|
+
/** 可声明的端 = 既有端名(单源 = 端名声明 `session-slots.mjs`),统一小写。`core` 不在其中——
|
|
20
|
+
* 它是「无人声明」的哨兵面,不是可声明的端。 */
|
|
21
|
+
const SURFACES = Object.freeze(["cli", "vscode", "desktop", "server"])
|
|
22
|
+
|
|
23
|
+
/** 进程级单值(初值 = 哨兵;见档头注)。 */
|
|
24
|
+
let identity = { surface: "core", version: "0.0.0" }
|
|
25
|
+
|
|
26
|
+
/** 会话标识头的值位变量(PROVIDER.md §6.17):定制头值里的 `${sessionId}` 于每次请求装配时替换为
|
|
27
|
+
* 本进程会话 id。只认这一个固定串——别的 `${…}` 不是变量,不加变量词表。 */
|
|
28
|
+
const SESSION_REF = "${sessionId}"
|
|
29
|
+
|
|
30
|
+
/** 声明本进程的客户端标识(端壳在进程启动期一次)。两个入参都在声明点校验——场景 = 端接线笔误
|
|
31
|
+
* (surface 写错字面 / 漏传 version),抛在启动期求值处,不进生产请求(落哨兵或 `undefined`
|
|
32
|
+
* 会把笔误永久藏成「编程调用」与「版本未知」)。 */
|
|
33
|
+
export function setClientIdentity({ surface, version } = {}) {
|
|
34
|
+
if (!SURFACES.includes(surface)) {
|
|
35
|
+
throw new Error(`setClientIdentity: unknown surface "${surface}" — expected one of: ${SURFACES.join(" | ")}`)
|
|
36
|
+
}
|
|
37
|
+
if (typeof version !== "string" || version.trim() === "") {
|
|
38
|
+
throw new Error(`setClientIdentity: missing version for surface "${surface}" — expected the end's package.json version string`)
|
|
39
|
+
}
|
|
40
|
+
identity = { surface, version }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 当前标识(副本——调用方零共享可变态)。 */
|
|
44
|
+
export function clientIdentity() {
|
|
45
|
+
return { ...identity }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 产品标识头值:`ThinCoder/<surface>/<version>`(三段固定——首段按产品名精确匹配即可聚合)。 */
|
|
49
|
+
export function userAgent() {
|
|
50
|
+
return `ThinCoder/${identity.surface}/${identity.version}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 值位模板解析(单串精确替换):整值与内嵌同解、可多处。"${sessionId}" 之外的 `${…}` 按构造原样
|
|
54
|
+
* 出站——不做变量识别,故没有「识别失败」这一支、也没有兜底路径(写法笔误出站就是该字面)。
|
|
55
|
+
* 非字符串值原样通过(同 `env-ref.mjs:29` 的类型门;配置链路经装载面净化只到字符串值,该门只兜
|
|
56
|
+
* 直接调本函数的面:单测 ∥ 编程调用)。 */
|
|
57
|
+
function resolveSessionRefs(value) {
|
|
58
|
+
return typeof value === "string" ? value.replaceAll(SESSION_REF, getSessionId()) : value
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 装配面入口:① 解析值位模板 `${sessionId}` → ② **删任何大小写变体的 `user-agent` 键** →
|
|
62
|
+
* ③ 写本产品值。消费形态 = 各 transport 的 header 对象内联展开:
|
|
63
|
+
* `{ ...withUserAgent(provider.headers), ...内置头 }`。 */
|
|
64
|
+
export function withUserAgent(headers = {}) {
|
|
65
|
+
const out = {}
|
|
66
|
+
for (const [key, value] of Object.entries(headers ?? {})) out[key] = resolveSessionRefs(value)
|
|
67
|
+
for (const key of Object.keys(out)) if (key.toLowerCase() === "user-agent") delete out[key]
|
|
68
|
+
out["User-Agent"] = userAgent()
|
|
69
|
+
return out
|
|
70
|
+
}
|
package/config-presets.mjs
CHANGED
|
@@ -51,9 +51,11 @@ export const PROVIDER_PRESETS = {
|
|
|
51
51
|
// `/messages` 侧(同 key——模型按侧选)。字段集 = 最小面:`thinking` / `reasoningEffort` 不设 = 不发
|
|
52
52
|
// (D-13 同口径);`maxTokens` 仅 anthropic 侧设 `65536`(该 transport `max_tokens` 必发——不设即落规格
|
|
53
53
|
// 行值 131072,超渠道口径)。**待验(无 key——未实拉 `/models` ∕ 载荷未实测;接入后按实拉复核)**;
|
|
54
|
-
// `x-opencode-session` 头 =
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
// `x-opencode-session` 头 = **必需**(不带 ⇒ 上游回 `HTTP 400 {"type":"MissingSessionID"}`——实测见
|
|
55
|
+
// PROVIDER.md §6.17);值形 = 模板 `${sessionId}`,每次请求现取本进程会话 id(解析单源 = `withUserAgent`)。
|
|
56
|
+
// 两条预设都携缺省值:新装即带上,用户无需手改配置;不声明该头的其它渠道出站头零变化。
|
|
57
|
+
"opencode-go": { baseURL: "https://opencode.ai/zen/go/v1", headers: { "x-opencode-session": "thincoder-${sessionId}" }, desc: "OpenCode Go subscription (sk- key from opencode.ai/zen; OpenAI-compatible models — MiniMax/Qwen via opencode-go-anthropic)" },
|
|
58
|
+
"opencode-go-anthropic": { baseURL: "https://opencode.ai/zen/go/v1", format: "anthropic", maxTokens: 65536, headers: { "x-opencode-session": "thincoder-${sessionId}" }, desc: "OpenCode Go — Anthropic Messages side (same sk- key as opencode-go; MiniMax/Qwen models)" },
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/** Build the stored provider entry from a preset — strip the display field, keep the rest
|
package/config.mjs
CHANGED
|
@@ -46,11 +46,11 @@ import { positiveInt, warnContextOnce } from "./model-context.mjs"
|
|
|
46
46
|
// Window utilization ratio (CONTEXT-COMPACTION.md §6.2 / D-CC30 / §6.17 / D-CC32): auto threshold =
|
|
47
47
|
// min(usableWindow(window, reserve), ⌊window × COMPACT_RATIO⌋) — reserve is the output budget
|
|
48
48
|
// ACTUALLY sent as max_tokens (uncapped; §6.17 requestBudget is the single source). Rows with
|
|
49
|
-
// reserve
|
|
50
|
-
// window − reserve — overflow-free outranks the
|
|
51
|
-
//
|
|
52
|
-
// instructions, memory/doc search
|
|
53
|
-
const COMPACT_RATIO = 0.
|
|
49
|
+
// reserve < 40% of the window land on the ratio term (line = 60.0% of the window); larger
|
|
50
|
+
// reservations pull the line down to window − reserve — overflow-free outranks the ratio (the two
|
|
51
|
+
// are mathematically exclusive once reserve ≥ 40%, §6.2). Headroom above the line absorbs injected
|
|
52
|
+
// context (directory tree, git context, outline, project instructions, memory/doc search — 30–50K).
|
|
53
|
+
const COMPACT_RATIO = 0.6
|
|
54
54
|
|
|
55
55
|
/** Output reservation counted against the window: the value ACTUALLY sent as max_tokens —
|
|
56
56
|
* explicit `provider.maxTokens` when set (the transport faces send it verbatim), else the spec
|
|
@@ -64,9 +64,9 @@ export function outputReserve(provider, spec) {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Usable input window = window − output reservation. Degenerate guard: a reservation
|
|
67
|
-
* ≥ the window is non-physical (no input could ever fit — e.g.
|
|
68
|
-
*
|
|
69
|
-
* spec); collapsing the budget to 0 would be instant `context_limit` death — the very
|
|
67
|
+
* ≥ the window is non-physical (no input could ever fit — e.g. `doubao-seed-2-1-turbo-260628`:
|
|
68
|
+
* 256_000 window / 256_000 `maxOutput` (官方口径) ∥ the test harness's 1K window vs the
|
|
69
|
+
* default spec); collapsing the budget to 0 would be instant `context_limit` death — the very
|
|
70
70
|
* failure the reservation fold exists to prevent. Fall back to half the window.
|
|
71
71
|
* (2026-10-06——收口核验捕获:advisor-loop-split B5a 红。) */
|
|
72
72
|
export function usableWindow(context, reserve) {
|
|
@@ -93,7 +93,10 @@ export function usableWindow(context, reserve) {
|
|
|
93
93
|
* - usable: usableWindow(window, reserve) (degenerate half-window guard).
|
|
94
94
|
* - threshold: min(usable, ⌊window × COMPACT_RATIO⌋) — the compaction trigger line.
|
|
95
95
|
* - limit: window − reserve — hard prompt ceiling; the guard's ONLY numeric criterion field
|
|
96
|
-
* (always a number; negative = any prompt overflows).
|
|
96
|
+
* (always a number; negative = any prompt overflows). `absent` rows (the transport sends no
|
|
97
|
+
* max_tokens) take the floor max(window − reserve, threshold): only the degenerate
|
|
98
|
+
* reserve ≥ window rows move (bare value 0 ⇒ every prompt would be rejected), and for
|
|
99
|
+
* reserve < window threshold ≤ usable = window − reserve makes the floor a no-op. */
|
|
97
100
|
export function requestBudget(provider, spec) {
|
|
98
101
|
// windowSource = 五级链的同一次遍历出口(§6.15)——L1 / L3 命中也会如实报来源,不再二值猜。
|
|
99
102
|
// 调用方传 spec(core.mjs 已算过 providerSpec)时窗口值用它,来源仍读 resolution(恒等映射,不并类)。
|
|
@@ -108,15 +111,18 @@ export function requestBudget(provider, spec) {
|
|
|
108
111
|
const maxTokensSource = explicit != null ? "explicit" : anthropicAlwaysSends ? "spec" : "absent"
|
|
109
112
|
const reserve = maxTokens ?? resolved?.maxOutput ?? 0
|
|
110
113
|
const usable = usableWindow(window, reserve)
|
|
114
|
+
const threshold = Math.min(usable, Math.floor(window * COMPACT_RATIO))
|
|
115
|
+
const bare = window - reserve
|
|
111
116
|
return {
|
|
112
|
-
window, windowSource, maxTokens, maxTokensSource, reserve, usable,
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
window, windowSource, maxTokens, maxTokensSource, reserve, usable, threshold,
|
|
118
|
+
// §6.17 地板:absent 档 reserve 可能 ≥ window(退化行,bare ≤ 0)——裸式下任何 prompt 都判真、
|
|
119
|
+
// 该档永久发不出去;地板把它抬到 ≤ ⌊w/2⌋。非 absent 档逐位不变(仍是 bare)。
|
|
120
|
+
limit: maxTokensSource === "absent" ? Math.max(bare, threshold) : bare,
|
|
115
121
|
}
|
|
116
122
|
}
|
|
117
123
|
|
|
118
124
|
/** Derive compaction threshold; explicit is the value explicitly set in config file (takes priority),
|
|
119
|
-
* otherwise auto = requestBudget().threshold (§6.17 single source — min(usable, ⌊window × 0.
|
|
125
|
+
* otherwise auto = requestBudget().threshold (§6.17 single source — min(usable, ⌊window × 0.6⌋),
|
|
120
126
|
* reserve uncapped). Second param accepts EITHER a model name string (pure spec lookup — compat shape,
|
|
121
127
|
* no caller passes a string today) OR a provider object (providerSpec — the providers[].context
|
|
122
128
|
* override in K units is honored, PROVIDER.md §15 T-C2). */
|
package/context.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { chat } from "./provider/index.mjs"
|
|
15
15
|
import { buildCompressMessages } from "./compress-form.mjs"
|
|
16
|
-
import { contextUsage, estimateTokens, keepTailSize, splitHistory, tailBudgetTokens } from "./token-window.mjs"
|
|
16
|
+
import { contextUsage, estimateTokens, keepTailSize, splitHistory, SUMMARY_TOKEN_ESTIMATE, tailBudgetTokens } from "./token-window.mjs"
|
|
17
17
|
import { COMPACTION_PLACEHOLDER, withPlaceholderPrefix } from "./context-echo.mjs"
|
|
18
18
|
import { shrinkOversized } from "./context-degrade.mjs"
|
|
19
19
|
|
|
@@ -36,6 +36,18 @@ const COMPACTION_PREFIX =
|
|
|
36
36
|
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
37
37
|
"but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* 注记 + 占位的估算(§6.17 可达性门的 `note` 项):摘要正文取 `SUMMARY_TOKEN_ESTIMATE`(§6.9 的 ~1K 目标,常量单源 =
|
|
41
|
+
* `token-window.mjs`),注记前缀与占位句按 §6.3 估算式(`estimateTokens` 走同一 `estimateText`)——零新常数、零新算式。
|
|
42
|
+
* 残余方向:真实摘要超 1K 时本值低估 `post`(门可能放行一次压不到线下的压缩),不会反向误拦——守卫面仍抛三实数。
|
|
43
|
+
* 另:`applyCompression` 压完还会追加任务表回注与 plan 提醒(`agent.tasks` / `planMode` 非空时),本值同样不含——
|
|
44
|
+
* 同向低估(大任务表会话里 `post` 偏小),不会把本该压的格拦下。
|
|
45
|
+
*/
|
|
46
|
+
const NOTE_ESTIMATE = SUMMARY_TOKEN_ESTIMATE + estimateTokens([
|
|
47
|
+
{ role: "user", content: COMPACTION_PREFIX },
|
|
48
|
+
{ role: "assistant", content: COMPACTION_PLACEHOLDER },
|
|
49
|
+
])
|
|
50
|
+
|
|
39
51
|
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
40
52
|
export const COMPRESS_FAILURE_LIMIT = 3
|
|
41
53
|
|
|
@@ -150,7 +162,22 @@ function applyCompression(agent, headEnd, tailStart, note) {
|
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
/**
|
|
153
|
-
*
|
|
165
|
+
* 可达性门(CONTEXT-COMPACTION.md §6.17「压缩触发判据(可达性门)」):压完之后 `total` 能不能落到阈值之下。
|
|
166
|
+
* `post` 按 `applyCompression` 实际留下的形态估——head(KEEP_HEAD = 0 ⇒ 恒空)+ 注记 + 占位 + tail;`middle` = 本次会摘掉的
|
|
167
|
+
* 那段(与本次压缩同一次 `splitHistory` 产出)。压缩后 `_lastPromptTokens` 失效 ⇒ 只有估算支可比,故走 estimated 支并乘同一
|
|
168
|
+
* `_tokRatio`(与 `contextUsage` 的估算支同尺)。
|
|
169
|
+
* 为什么要有它:`system + tools` 是固定开销、压不掉,把它算进判据左侧会让「固定开销本身逼近阈值」的会话反复触发压缩却压不出
|
|
170
|
+
* 任何东西——每触发一次白付一次摘要请求(台账 `#1833`)。
|
|
171
|
+
*/
|
|
172
|
+
function postCompactionTotal(agent, usage, middle) {
|
|
173
|
+
const ratio = agent?._tokRatio ?? 1
|
|
174
|
+
return Math.round((usage.history - estimateTokens(middle) + NOTE_ESTIMATE + usage.overhead) * ratio)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Compact when the prompt is over the threshold AND compaction can actually get back under it
|
|
179
|
+
* (CONTEXT-COMPACTION.md §6.17 reachability gate); `extras.force` skips both early-exit criteria.
|
|
180
|
+
* Returns whether compaction happened.
|
|
154
181
|
* Only called at safe points in the loop (history ends with user or tool message — a complete exchange boundary).
|
|
155
182
|
* Automatically re-injects task list state after compaction.
|
|
156
183
|
* @param {object} agent
|
|
@@ -160,14 +187,14 @@ function applyCompression(agent, headEnd, tailStart, note) {
|
|
|
160
187
|
* internal mechanism, not a model reply); onCompressStart fires right before the summary call
|
|
161
188
|
* (§6.8 D-C1, compression lifecycle visibility — panel start state)
|
|
162
189
|
* @param {object} extras - { systemPrompt?, tools?, force?, focus? } — 固定开销面(system + tools 估计)+
|
|
163
|
-
* 模型主动压缩面(§6.16.2):`force`
|
|
190
|
+
* 模型主动压缩面(§6.16.2):`force` **跳过阈值面的全部早退判据**(`total <= threshold` 与 §6.17 可达性门的
|
|
191
|
+
* `post > threshold` 两条),其余全同(同切分 / 同尾部预算 / 同摘要链 / 同回注 / 同基线失效);`focus` 追加在摘要指令尾段。
|
|
164
192
|
*/
|
|
165
193
|
export async function compressIfNeeded(agent, threshold, callbacks, extras = {}, signal) {
|
|
166
194
|
const history = agent.history
|
|
167
|
-
// 单源(§6.16.4):total
|
|
195
|
+
// 单源(§6.16.4):total 与 stats 面同一函数(既有内联式改调 contextUsage,判定语义零改)
|
|
168
196
|
const usage = contextUsage(agent, extras)
|
|
169
197
|
const tokens = usage.total
|
|
170
|
-
const overhead = usage.overhead
|
|
171
198
|
if (!extras.force && tokens <= threshold) return false
|
|
172
199
|
|
|
173
200
|
const keepTail = keepTailSize(agent.provider, history.length)
|
|
@@ -179,6 +206,9 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
179
206
|
}
|
|
180
207
|
|
|
181
208
|
const middle = history.slice(split.headEnd, split.tailStart)
|
|
209
|
+
// 可达性门(§6.17):压完落不到线下 ⇒ 不压——不为一次没有净减的摘要白付请求。`force` 全文豁免(模型主动压缩 ∥
|
|
210
|
+
// 守卫强压:模型判定优先于阈值,压后仍超由守卫面抛三实数)。到不了线下的格仍由守卫 `enforceContextFit` 兜住。
|
|
211
|
+
if (!extras.force && postCompactionTotal(agent, usage, middle) > threshold) return false
|
|
182
212
|
|
|
183
213
|
// Compression request form v2/v3 (§6.14 / D-CC20): continuation — same system + same tools declaration (the turn's own
|
|
184
214
|
// array) + the middle's verbatim messages + one tail instruction. 首现即复用回合所建前缀 ⟺ 带 tools 声明(无 tool_choice)
|
|
@@ -222,11 +252,12 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
222
252
|
|
|
223
253
|
// Completion info for the compression panel (D-C2): tokens freed = the pre-compression prompt
|
|
224
254
|
// estimate (`tokens` — the value that tripped the threshold, incl. system/tools overhead on the
|
|
225
|
-
// pure-estimation path) minus the post-compression estimate on the
|
|
226
|
-
//
|
|
255
|
+
// pure-estimation path) minus the post-compression estimate on the SAME basis (`contextUsage`
|
|
256
|
+
// — a bare `estimateTokens` sum would skip the §6.17 `_tokRatio` and overstate the freed tokens).
|
|
257
|
+
// Elapsed = the summary call + splice duration. agent.mjs forwards this to onCompress unchanged.
|
|
227
258
|
agent._lastCompressInfo = {
|
|
228
259
|
mode: "summary",
|
|
229
|
-
tokensFreed: Math.max(0, Math.round(tokens - (
|
|
260
|
+
tokensFreed: Math.max(0, Math.round(tokens - contextUsage(agent, extras).total)),
|
|
230
261
|
elapsedMs: performance.now() - startedAt,
|
|
231
262
|
}
|
|
232
263
|
return true
|
package/declaration.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* 原档 `conventions.mjs` 经 `export { … } from` 转口保名(消费面 / 单元测试档 import 面零改)。
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
9
|
-
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
9
|
+
import { basename, dirname, isAbsolute, join, parse, relative, resolve } from "node:path"
|
|
10
10
|
import { DEFAULT_MANIFEST, MANIFEST_REL, docRootPaths, manifestFilePath, onManifestInit, owningProject, readManifest } from "./manifest.mjs"
|
|
11
11
|
import { isSkippedRelPath } from "./memory/file-walk.mjs" // 面⑦ 子目录扫描的跳过段谓词单源(零环:file-walk 不引本档)
|
|
12
12
|
import { logEvent } from "./log.mjs"
|
|
@@ -365,7 +365,7 @@ export function loadProjectDeclaration(cwd) {
|
|
|
365
365
|
* @returns {Readonly<object>|null} 所属项目的声明对象 / null = 出辖
|
|
366
366
|
*/
|
|
367
367
|
export function declarationForTarget(targetAbs) {
|
|
368
|
-
const root = owningProject(targetAbs)
|
|
368
|
+
const root = owningProject(targetAbs, parse(targetAbs).root) // 取根面的界 = 目标所在盘根(与 `projectRootView` 同一口径)
|
|
369
369
|
if (!root) return null
|
|
370
370
|
return loadProjectDeclaration(root)
|
|
371
371
|
}
|