@thincoder/core 0.9.5 → 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 +49 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +12 -12
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +22 -12
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +54 -296
- package/agent/family-tools.mjs +5 -5
- package/agent/helpers.mjs +53 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +15 -11
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +89 -29
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +9 -9
- package/agent-tools/advisor-settle.mjs +12 -12
- package/agent-tools/advisor.mjs +36 -41
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +21 -18
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +57 -18
- package/agent-tools/batch-paths.mjs +52 -8
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +12 -65
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +56 -56
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +16 -11
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +2 -2
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +21 -265
- package/agent-tools/subagent-async.mjs +25 -20
- package/agent-tools/subagent-panel.mjs +33 -4
- package/agent-tools/subagent-run.mjs +36 -21
- package/agent-tools/subagent-scheduler.mjs +28 -27
- package/agent-tools/subagent-spawn.mjs +45 -14
- package/agent-tools/subagent.mjs +33 -53
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +18 -355
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +11 -6
- package/config-watch.mjs +83 -0
- package/config.mjs +29 -12
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +10 -2
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +9 -4
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-ref.mjs +96 -3
- package/model-specs.mjs +16 -7
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +2 -2
- package/peer-domains.mjs +7 -11
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +16 -8
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +2 -3
- package/prompts/persona-engineering.md +32 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +1 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +108 -27
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -241
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +4 -4
- package/tools/edit-diff.mjs +4 -23
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +4 -9
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/shared.mjs +8 -6
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
package/config-watch.mjs
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config-watch.mjs — config.json 外部写盘感知·**纯逻辑面**(R8 · 桌面功能对位批「上提」KD-T2——
|
|
3
|
+
* 自 `thincoder-vscode/src/extension/config-watch.mjs:35-77` 逐字随迁:去抖 300ms ∕ stat 元组
|
|
4
|
+
* (`mtimeMs:size`)比对 ∕ 自写抑制;原批 = 第 21 批 B5 · `docs/design/SETTINGS.md` §2.6)。
|
|
5
|
+
*
|
|
6
|
+
* `~/.thincoder/config.json` 为多端共享单文件:CLI(`/advisor`、`settings set`)或手工编辑写盘后,
|
|
7
|
+
* 常开面板原本零感知(只在打开面板/保存时拉快照)。机制(零语义改):
|
|
8
|
+
* 宿主 fs 事件 →(端落子)`onEvent` → 去抖 → 元组比对 → 变化才 `onChange`(事件驱动零空转)。
|
|
9
|
+
* 自写抑制:本端自身写盘同样改元组 ⇒ 裸推送会在用户编辑中被快照重建;写盘成功后核 `config-io`
|
|
10
|
+
* `saveRaw` 同步回调 `onConfigSelfWrite`(`config-io.mjs:107`)⇒ `noteSelfWrite()` 把基线刷到当前
|
|
11
|
+
* 元组 ⇒ 事件到达时元组已等于基线 ⇒ 零推送;外部写无回调 ⇒ 元组异于基线 ⇒ 推送。
|
|
12
|
+
*
|
|
13
|
+
* 平台面两注入(核内零宿主绑定):① `attach`(fs 事件源工厂——端落子:VSC = `createFileSystemWatcher`、
|
|
14
|
+
* 桌面 = `node:fs.watch`)② `setTimer` ∕ `clearTimer`(定时器;缺省 = 全局)。`tupleOf`(stat 探针)缺省 =
|
|
15
|
+
* 本核 `statSync` 实现(两端宿主皆 Node);`subscribeSelfWrite` 缺省 = 核自写订阅。
|
|
16
|
+
* 降级(无 path ∕ `attach` 缺失 ∕ 构造抛错 ∕ 返回空)⇒ no-op 面(不阻断端激活——面板打开拉新的既有路径兜底)。
|
|
17
|
+
* 装配序微差(对源件):事件订阅随 `attach` 一次落(源件 = 构造 → 基线 → 订阅三事件)——订阅更早,
|
|
18
|
+
* 不丢事件;基线仍于 `attach` 后取(同步段内零窗口)。零语义差。
|
|
19
|
+
*/
|
|
20
|
+
import { statSync } from "node:fs"
|
|
21
|
+
import { _configPath, onConfigSelfWrite } from "./config-io.mjs"
|
|
22
|
+
|
|
23
|
+
/** stat 元组(mtimeMs + size——同 tick 快写 mtime 可同,size 兜底);缺失 ∕ 不可读 ⇒ null。 */
|
|
24
|
+
export function configTupleOf(path) {
|
|
25
|
+
try {
|
|
26
|
+
const s = statSync(path)
|
|
27
|
+
return `${s.mtimeMs}:${s.size}`
|
|
28
|
+
} catch { return null }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 建配置监视面(纯逻辑)。
|
|
33
|
+
* @param {{ configPath?: string, onChange?: () => void, debounceMs?: number,
|
|
34
|
+
* attach?: (arg: { path: string, onEvent: () => void }) => { dispose?: () => void } | null,
|
|
35
|
+
* tupleOf?: (path: string) => string | null,
|
|
36
|
+
* setTimer?: (fn: () => void, ms: number) => unknown, clearTimer?: (handle: unknown) => void,
|
|
37
|
+
* subscribeSelfWrite?: (fn: () => void) => (() => void) | void }} [opts]
|
|
38
|
+
* `configPath` 缺省 = 核 `_configPath()` 当前值(测试缝同源);`attach` = 平台落子(fs 事件源)。
|
|
39
|
+
* @returns {{ dispose: () => void, noteSelfWrite: () => void }} 降级时返回 no-op(不阻断激活)。
|
|
40
|
+
*/
|
|
41
|
+
export function createConfigWatch({
|
|
42
|
+
configPath, onChange, debounceMs = 300, attach, tupleOf = configTupleOf,
|
|
43
|
+
setTimer = setTimeout, clearTimer = clearTimeout, subscribeSelfWrite = onConfigSelfWrite,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const noop = { dispose() {}, noteSelfWrite() {} }
|
|
46
|
+
const path = configPath || _configPath()
|
|
47
|
+
if (!path) return noop
|
|
48
|
+
if (typeof attach !== "function") return noop
|
|
49
|
+
|
|
50
|
+
let baseline = null
|
|
51
|
+
let timer = null
|
|
52
|
+
/** 元组比对(去抖落点):未变 → 零推送(隐藏 N5 判据保持);变 ⇒ 基线刷新 + `onChange`(抛不炸宿主)。 */
|
|
53
|
+
const check = () => {
|
|
54
|
+
timer = null
|
|
55
|
+
const now = tupleOf(path)
|
|
56
|
+
if (now === baseline) return
|
|
57
|
+
baseline = now // 比对后基线更新为当前元组
|
|
58
|
+
try { onChange?.() } catch (e) { console.warn(`[config-watch] onChange failed: ${e.message}`) }
|
|
59
|
+
}
|
|
60
|
+
/** 去抖(同拍多事件合并——窗口内重排)。 */
|
|
61
|
+
const schedule = () => {
|
|
62
|
+
if (timer) clearTimer(timer)
|
|
63
|
+
timer = setTimer(check, debounceMs)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let listener
|
|
67
|
+
try { listener = attach({ path, onEvent: schedule }) } catch { return noop }
|
|
68
|
+
if (!listener) return noop
|
|
69
|
+
baseline = tupleOf(path)
|
|
70
|
+
const noteSelfWrite = () => { baseline = tupleOf(path) }
|
|
71
|
+
|
|
72
|
+
let unsubSelfWrite = null
|
|
73
|
+
try { unsubSelfWrite = subscribeSelfWrite(noteSelfWrite) } catch { /* 订阅失败仅弱化自写抑制 */ }
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
noteSelfWrite,
|
|
77
|
+
dispose() {
|
|
78
|
+
try { listener.dispose?.() } catch { /* already disposed */ }
|
|
79
|
+
if (timer) { clearTimer(timer); timer = null }
|
|
80
|
+
try { unsubSelfWrite?.() } catch { /* ignore */ }
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
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 }
|
|
@@ -48,7 +48,7 @@ export const DEFAULTS = {
|
|
|
48
48
|
streamRules: [], // time-traveling stream rules: [{ pattern: "regex", message: "reminder", action: "abort"|"warn", repeat: "always"|"once" }]
|
|
49
49
|
advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
|
|
50
50
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
51
|
-
engineering: false, //
|
|
51
|
+
engineering: false, // initial default only — the runtime value is the session authority (MANIFEST.md §2.2 / ENGINEERING-MODE-V2.md §2.3 E5.1). Strict methodology enforcement — design-before-code (design review + user approval before code)
|
|
52
52
|
// Async pool limits (AGENT-LOOP-ASYNC-POOL.md §6.10 D-24a/R14 + R13 — POOL-CONFIG-
|
|
53
53
|
// UNIFIED 2026-09-09): { engCoder, other, advisor } — eng-coder pool / other-role
|
|
54
54
|
// pool / advisor-review pool, defaults 4/4/4 (user ruling "eng-coder 四路,其他
|
|
@@ -61,6 +61,9 @@ export const DEFAULTS = {
|
|
|
61
61
|
// ADVISOR_POOL_LIMIT 逐键同值(运行时回退常量)——耦合锚 T-24a4 断言锁住——
|
|
62
62
|
// 勿单侧改默认。
|
|
63
63
|
poolLimits: { engCoder: 4, other: 4, advisor: 4 },
|
|
64
|
+
// §6.30.3 门三件③(D-TW5):timer 空闲自唤醒开关——默认开(工具描述面已承诺到点提醒);
|
|
65
|
+
// false ⇒ 端侧不武装闩(回既有回合边界投递——零调度器面)。键面随 DEFAULTS 派生进 settings 类型表。
|
|
66
|
+
timerWake: true,
|
|
64
67
|
},
|
|
65
68
|
memory: {
|
|
66
69
|
dbPath: join(configDir, "memory.db"),
|
|
@@ -83,6 +86,10 @@ export const DEFAULTS = {
|
|
|
83
86
|
enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
|
|
84
87
|
retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
|
|
85
88
|
},
|
|
89
|
+
// 诊断开关(用户面 ① 配置键——CONFIG.md §6.2 名录):`heapWatch` 默认开(设 false 关闭)——自愈臂
|
|
90
|
+
// 遥测;`heapSnapshot` 默认关——采集 opt-in(堆转储敏感度 ≥ traces ⇒ 对齐发布隐私先例 D-TR6;
|
|
91
|
+
// 2026-09-30 采集收网批;桌面端运行期热读 ∥ CLI 本进程下次启动)
|
|
92
|
+
diagnostics: { heapWatch: true, heapSnapshot: false },
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
@@ -127,11 +134,12 @@ export function isBailianHost(baseURL) {
|
|
|
127
134
|
* provider.reasoningEffort → true (effort tier implies thinking on; rides with reasoning_effort)
|
|
128
135
|
* otherwise → undefined (field omitted — server default stays, no behavior change)
|
|
129
136
|
* NOTE: spec carries no model field today — the name comes from provider.model (spec?.model is
|
|
130
|
-
* a forward-compatible fallback).
|
|
131
|
-
*
|
|
137
|
+
* a forward-compatible fallback). 非串 model(脏载 / 手工档)归一等价空串(未登记 ⇒ undefined)——
|
|
138
|
+
* 本函数恒 total、不因类型脏载抛(#638 查表族同归一)。
|
|
132
139
|
*/
|
|
133
140
|
export function resolveEnableThinking(provider, spec) {
|
|
134
|
-
const
|
|
141
|
+
const raw = provider?.model ?? spec?.model ?? ""
|
|
142
|
+
const model = (typeof raw === "string" ? raw : "").toLowerCase()
|
|
135
143
|
if (!model.startsWith("qwen") || model.startsWith("qwen3-coder")) return undefined
|
|
136
144
|
if (!isBailianHost(provider?.baseURL)) return undefined
|
|
137
145
|
if (provider.thinking === null) return false
|
|
@@ -257,6 +265,7 @@ export function loadConfig() {
|
|
|
257
265
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
258
266
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
259
267
|
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
268
|
+
diagnostics: { ...DEFAULTS.diagnostics },
|
|
260
269
|
}
|
|
261
270
|
|
|
262
271
|
// 家目录展开(第 29 批):config 路径字段单一规范化点——只读归一(磁盘原文保留)
|
|
@@ -268,6 +277,10 @@ export function loadConfig() {
|
|
|
268
277
|
}
|
|
269
278
|
merged.shell = expandHome(merged.shell)
|
|
270
279
|
|
|
280
|
+
// diagnostics(① 配置键,CONFIG.md §6.2 名录):两键只认布尔值——非法 / 缺失回退默认(不 throw)
|
|
281
|
+
const _diagRaw = config.diagnostics && typeof config.diagnostics === "object" && !Array.isArray(config.diagnostics) ? config.diagnostics : {}
|
|
282
|
+
for (const key of ["heapWatch", "heapSnapshot"]) if (typeof _diagRaw[key] === "boolean") merged.diagnostics[key] = _diagRaw[key]
|
|
283
|
+
|
|
271
284
|
// providers[].model 内存归一(v2 M3:非空字符串保留;非字符串/空串归一删除)——渠道默认模型
|
|
272
285
|
// 单值;无默认模型合法(模型选择经 /models 拉取候选——准入判据见 M8/M9)。
|
|
273
286
|
for (const p of merged.providers) {
|
|
@@ -312,14 +325,18 @@ export function loadConfig() {
|
|
|
312
325
|
// 保证 agent.config.proxy 永远是规范形态或 undefined
|
|
313
326
|
merged.proxy = normalizeProxy(merged.proxy)
|
|
314
327
|
|
|
315
|
-
// Runtime provider =
|
|
316
|
-
//
|
|
317
|
-
// 标记 → TUI 首帧弹选择 / headless 报可读错误;同 2026-09-02 Q1 语义——不复用
|
|
318
|
-
// throw 契约——findProvider 保留给 advisor/run.mjs 等直接调用方)。
|
|
319
|
-
|
|
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 消费面照旧)
|
|
320
337
|
merged.providerInvalidReason = merged.provider.name
|
|
321
338
|
? null
|
|
322
|
-
: defaultModelReason(merged.providers, merged.defaultModel)
|
|
339
|
+
: plan.reason ?? defaultModelReason(merged.providers, merged.defaultModel)
|
|
323
340
|
|
|
324
341
|
// Compaction threshold follows the model (provider-level context override honored — providerSpec)
|
|
325
342
|
const explicitThreshold = config.agent?.compactThreshold
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-degrade.mjs — 确定性降级面(无 LLM 的有损收缩 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
|
|
3
|
+
* 内容 = `pruneStub` ∥ `pruneStaleToolOutputs`(F-CC3 陈旧工具输出清理)∥ `OVERSIZE_CONTENT_LIMIT` ∥
|
|
4
|
+
* `shrinkOversized`(splitHistory 无中间段时的单条超限收缩)——迁出块逐字。
|
|
5
|
+
* 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
|
|
6
|
+
* 跨档被引的原私有面(`shrinkOversized`)补 `export` 关键字。
|
|
7
|
+
*/
|
|
8
|
+
import { collectStaleToolOutputs } from "./token-window.mjs"
|
|
9
|
+
|
|
10
|
+
/** prune stub 逐字(§6.16.3):只给「已清理 + 原长度 + 重跑路径」——prune 是**删**不是摘要,不声称可复原。 */
|
|
11
|
+
const pruneStub = (chars) => `[pruned: stale tool output dropped (${chars} chars) — re-run the tool if you need it again.]`
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 陈旧工具输出清理(F-CC3 · §6.16.3):合格集 = `token-window.mjs` 单源(保护尾外 ∧ `role:"tool"` ∧ ≥ 门槛);
|
|
15
|
+
* 命中项**原位换「内容」**(`history[i] = { ...m, content: stub }`——数组引用 / 长度 / 索引 / `tool_call_id` 全不变
|
|
16
|
+
* ⇒ 配对**结构上不可能被拆**)。记录面零改(copy-on-write:消息对象与人读线共享);基线失效同 `shrinkOversized` 先例。
|
|
17
|
+
* @returns {{pruned:number, freed:number, candidates:number, tailKept:number, belowMin:number}}
|
|
18
|
+
*/
|
|
19
|
+
export function pruneStaleToolOutputs(agent) {
|
|
20
|
+
const history = agent.history
|
|
21
|
+
const stale = collectStaleToolOutputs(history, agent.provider)
|
|
22
|
+
const counts = {
|
|
23
|
+
pruned: stale.indexes.length,
|
|
24
|
+
freed: stale.tokens,
|
|
25
|
+
candidates: stale.candidates,
|
|
26
|
+
tailKept: stale.tailKept,
|
|
27
|
+
belowMin: stale.belowMin,
|
|
28
|
+
}
|
|
29
|
+
if (counts.pruned === 0) return counts
|
|
30
|
+
for (const i of stale.indexes) {
|
|
31
|
+
const m = history[i]
|
|
32
|
+
// 多模态 tool 结果(content 数组)整体替换为 stub ⇒ 图像 part 丢弃(不可再取——prune 是删;回执只给重跑路径)
|
|
33
|
+
const chars = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content ?? "").length
|
|
34
|
+
history[i] = { ...m, content: pruneStub(chars) }
|
|
35
|
+
}
|
|
36
|
+
agent._lastPromptTokens = null
|
|
37
|
+
agent._usageAtLen = null
|
|
38
|
+
return counts
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
|
|
42
|
+
const OVERSIZE_CONTENT_LIMIT = 8_000
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
|
|
46
|
+
* Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
|
|
47
|
+
* does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
|
|
48
|
+
* risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
|
|
49
|
+
* committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
|
|
50
|
+
* Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
|
|
51
|
+
*/
|
|
52
|
+
export function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
53
|
+
let shrunk = false
|
|
54
|
+
// Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
|
|
55
|
+
// message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
|
|
56
|
+
// line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
|
|
57
|
+
// the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
|
|
58
|
+
// already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
|
|
59
|
+
const next = agent.history.map((m) => {
|
|
60
|
+
if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
|
|
61
|
+
if (m.content.length <= limit) return m
|
|
62
|
+
// Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
|
|
63
|
+
const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
|
|
64
|
+
const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
|
|
65
|
+
shrunk = true
|
|
66
|
+
return {
|
|
67
|
+
...m,
|
|
68
|
+
content:
|
|
69
|
+
m.content.slice(0, keepHead) +
|
|
70
|
+
`\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
|
|
71
|
+
m.content.slice(-keepTail),
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
if (shrunk) {
|
|
75
|
+
agent.history = next
|
|
76
|
+
// Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
|
|
77
|
+
agent._lastPromptTokens = null
|
|
78
|
+
agent._usageAtLen = null
|
|
79
|
+
}
|
|
80
|
+
return shrunk
|
|
81
|
+
}
|
package/context-echo.mjs
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-echo.mjs — 回声安全面(D-CC18 ∥ D-CC19 恢复面归并 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
|
|
3
|
+
* 内容 = `COMPACTION_PLACEHOLDER` ∥ 前缀器族(`prefixContent` / `withPlaceholderPrefix`)∥ 回声对归并
|
|
4
|
+
* (`isAssistantEchoPair` / `contentTextOf` / `absorbEchoContent` / `mergeAdjacentAssistantEchoes`)——迁出块逐字。
|
|
5
|
+
* 原档 `context.mjs` 经 `export { … } from` 转口保名(消费面 / 批内件 import 面零改);
|
|
6
|
+
* 跨档被引的原私有面(`COMPACTION_PLACEHOLDER` ∥ `withPlaceholderPrefix`)补 `export` 关键字。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
|
|
10
|
+
export const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
|
|
14
|
+
* string → text + blank line + original; multimodal array → text part prepended;
|
|
15
|
+
* empty string / null / undefined / other → text alone.
|
|
16
|
+
*/
|
|
17
|
+
function prefixContent(content, text) {
|
|
18
|
+
if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
|
|
19
|
+
if (Array.isArray(content)) return [{ type: "text", text }, ...content]
|
|
20
|
+
return text
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
|
|
25
|
+
* Thin wrapper over prefixContent (behavioral semantics unchanged).
|
|
26
|
+
*/
|
|
27
|
+
export function withPlaceholderPrefix(content) {
|
|
28
|
+
return prefixContent(content, COMPACTION_PLACEHOLDER)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
|
|
33
|
+
* is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
|
|
34
|
+
* WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
|
|
35
|
+
* mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
|
|
36
|
+
* only at restore time, never prompts the user, never rewrites the session file. Merge direction
|
|
37
|
+
* matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
|
|
38
|
+
* tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
|
|
39
|
+
* line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
|
|
40
|
+
* shared with other lines, so the merged message is always a NEW object; clean input returns
|
|
41
|
+
* the SAME array reference (zero copy).
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
|
|
45
|
+
* directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
|
|
46
|
+
* safety, F-3.) */
|
|
47
|
+
export function isAssistantEchoPair(prev, next) {
|
|
48
|
+
return prev?.role === "assistant"
|
|
49
|
+
&& next?.role === "assistant"
|
|
50
|
+
&& !prev.reasoning_content
|
|
51
|
+
&& !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Text of a message content in any supported shape (string / parts array / null-ish). */
|
|
55
|
+
function contentTextOf(content) {
|
|
56
|
+
if (typeof content === "string") return content
|
|
57
|
+
if (Array.isArray(content)) {
|
|
58
|
+
return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
|
|
59
|
+
}
|
|
60
|
+
return ""
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
|
|
64
|
+
function absorbEchoContent(prevContent, nextContent) {
|
|
65
|
+
const text = contentTextOf(prevContent)
|
|
66
|
+
if (text.length === 0) return nextContent
|
|
67
|
+
return prefixContent(nextContent, text)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function mergeAdjacentAssistantEchoes(history) {
|
|
71
|
+
if (!Array.isArray(history)) return history
|
|
72
|
+
let src = history
|
|
73
|
+
for (;;) {
|
|
74
|
+
let changed = false
|
|
75
|
+
const next = []
|
|
76
|
+
for (let i = 0; i < src.length; i++) {
|
|
77
|
+
if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
|
|
78
|
+
next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
|
|
79
|
+
i += 1
|
|
80
|
+
changed = true
|
|
81
|
+
} else {
|
|
82
|
+
next.push(src[i])
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!changed) return src === history ? history : src
|
|
86
|
+
src = next
|
|
87
|
+
}
|
|
88
|
+
}
|
package/context-push.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context-push.mjs — 消息双线写入单点(写缝面 · 自 `context.mjs` 迁出 · 2026-10-01 core 拆分批 #755 ∥ #786)。
|
|
3
|
+
* `pushReal` = 真实消息写缝(机器线 `agent.history` + 人读线 `agent._fullHistory` 同源双写);
|
|
4
|
+
* `pushRecord` = 人读线留档记录单点(机器线零触)。迁出块逐字;原档 `context.mjs` 经
|
|
5
|
+
* `export { … } from` 转口保名(消费面 / 批内件 import 面零改)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* pushReal — the single entry point for REAL conversation messages.
|
|
10
|
+
* A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
|
|
11
|
+
* agent.history — the machine context (compaction shrinks this)
|
|
12
|
+
* agent._fullHistory — the human-readable record (persistence source)
|
|
13
|
+
* Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
|
|
14
|
+
* are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
|
|
15
|
+
* The two lines are written independently at the source — no after-the-fact delta sync.
|
|
16
|
+
* Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
|
|
17
|
+
* point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
|
|
18
|
+
* is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
|
|
19
|
+
* ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
|
|
20
|
+
*
|
|
21
|
+
* TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
|
|
22
|
+
* ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
|
|
23
|
+
* ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
|
|
24
|
+
* 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
|
|
25
|
+
* 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
|
|
26
|
+
*/
|
|
27
|
+
export function pushReal(agent, msg) {
|
|
28
|
+
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
29
|
+
if (msg && msg.ts === undefined) msg.ts = Date.now()
|
|
30
|
+
agent._fullHistory.push(msg)
|
|
31
|
+
try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
|
|
32
|
+
const win = agent._historyWindow
|
|
33
|
+
if (win > 0 && agent._fullHistory.length > win) {
|
|
34
|
+
agent._fullHistory.splice(0, agent._fullHistory.length - win)
|
|
35
|
+
}
|
|
36
|
+
agent.history.push(msg)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* pushRecord — 人读线留档记录单点(消化生命周期面 · #726;形 ∥ 写缝 ∥ 判据单源 = SESSION.md §6.26)。
|
|
41
|
+
* `pushReal` 双胞(直复用——零算法副本):`ts` 打点 ∥ 记录存储追加(`_recordStore?.append`——§6.14)∥
|
|
42
|
+
* 尾窗驱逐三面同源;**机器线零触** = `history` 以一次性弃数组承接 ⇒ 记录不入 `agent.history` /
|
|
43
|
+
* `contextHistory`(不喂模型)。未绑定(`_recordStore` 缺)⇒ 人读线追加照常 ∥ 存储腿空转(零抛——模式 F
|
|
44
|
+
* 零回归);载体缺位(无活跃会话)⇒ 零动作(零抛——日志归端侧)。
|
|
45
|
+
*/
|
|
46
|
+
export function pushRecord(agent, record) {
|
|
47
|
+
if (!agent || typeof agent !== "object") return // 载体缺位:零动作(零抛——§6.26 失败面)
|
|
48
|
+
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
49
|
+
// 载体形人读线半提取(先例 = 桌面 `session-io.appendRecord`):`history` 弃数组承接 ⇒ 机器线零触
|
|
50
|
+
pushReal({ _fullHistory: agent._fullHistory, _recordStore: agent._recordStore, _historyWindow: agent._historyWindow, history: [] }, record)
|
|
51
|
+
}
|
package/context.mjs
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
import { chat } from "./provider/index.mjs"
|
|
15
15
|
import { buildCompressMessages } from "./compress-form.mjs"
|
|
16
|
-
import { contextUsage, estimateTokens,
|
|
16
|
+
import { contextUsage, estimateTokens, keepTailSize, splitHistory, tailBudgetTokens } from "./token-window.mjs"
|
|
17
|
+
import { COMPACTION_PLACEHOLDER, withPlaceholderPrefix } from "./context-echo.mjs"
|
|
18
|
+
import { shrinkOversized } from "./context-degrade.mjs"
|
|
17
19
|
|
|
18
20
|
export const SUMMARIZE_PROMPT = `The conversation above is our work log so far — summarize it into a compact summary for use as context in the ongoing conversation.
|
|
19
21
|
Requirements:
|
|
@@ -34,9 +36,6 @@ const COMPACTION_PREFIX =
|
|
|
34
36
|
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
35
37
|
"but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
|
|
36
38
|
|
|
37
|
-
/** Placeholder assistant reply committed right after compaction (D9); D-CC18 merges it into an adjacent tail assistant instead of emitting it as a separate message */
|
|
38
|
-
const COMPACTION_PLACEHOLDER = "Understood. I'll continue from these notes, re-verifying anything transient."
|
|
39
|
-
|
|
40
39
|
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
41
40
|
export const COMPRESS_FAILURE_LIMIT = 3
|
|
42
41
|
|
|
@@ -71,115 +70,6 @@ function anchorText(agent) {
|
|
|
71
70
|
return lines.length > 0 ? lines.join("\n") : null
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/**
|
|
75
|
-
* pushReal — the single entry point for REAL conversation messages.
|
|
76
|
-
* A real message (user input, assistant reply, tool result, multimodal image) is appended to BOTH:
|
|
77
|
-
* agent.history — the machine context (compaction shrinks this)
|
|
78
|
-
* agent._fullHistory — the human-readable record (persistence source)
|
|
79
|
-
* Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
|
|
80
|
-
* are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
|
|
81
|
-
* The two lines are written independently at the source — no after-the-fact delta sync.
|
|
82
|
-
* Message timestamps (SESSION.md §6.9): stamped HERE once at push time (epoch ms) — a single
|
|
83
|
-
* point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
|
|
84
|
-
* is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
|
|
85
|
-
* ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
|
|
86
|
-
*
|
|
87
|
-
* TUI-OOM-ROOTCAUSE 批(SESSION.md §6.14)——人读线内存有界 + 磁盘为准:
|
|
88
|
-
* ① `agent._recordStore?.append(msg)`:记录同步追加(磁盘为准——append-only sidecar);
|
|
89
|
-
* ② 窗口驱逐:绑定态(agent._historyWindow = 200)下 _fullHistory 只保最近窗口条——
|
|
90
|
-
* 更早内容仅存磁盘(翻页/检索/保存从盘按需读)。未绑定(模式 F)不驱逐(零回归)。
|
|
91
|
-
* 追加失败不阻断回合(独立 try/catch——尽力面 N-S6;store 内部另置 degraded 并停写)。
|
|
92
|
-
*/
|
|
93
|
-
export function pushReal(agent, msg) {
|
|
94
|
-
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
95
|
-
if (msg && msg.ts === undefined) msg.ts = Date.now()
|
|
96
|
-
agent._fullHistory.push(msg)
|
|
97
|
-
try { agent._recordStore?.append(msg) } catch { /* 尽力面:落盘失败不阻断回合(N-S6) */ }
|
|
98
|
-
const win = agent._historyWindow
|
|
99
|
-
if (win > 0 && agent._fullHistory.length > win) {
|
|
100
|
-
agent._fullHistory.splice(0, agent._fullHistory.length - win)
|
|
101
|
-
}
|
|
102
|
-
agent.history.push(msg)
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Content-shape-safe prefixing (D-CC18, generalized for D-CC19 merge reuse):
|
|
107
|
-
* string → text + blank line + original; multimodal array → text part prepended;
|
|
108
|
-
* empty string / null / undefined / other → text alone.
|
|
109
|
-
*/
|
|
110
|
-
function prefixContent(content, text) {
|
|
111
|
-
if (typeof content === "string" && content.length > 0) return `${text}\n\n${content}`
|
|
112
|
-
if (Array.isArray(content)) return [{ type: "text", text }, ...content]
|
|
113
|
-
return text
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* D-CC18 echo safety: prefix the placeholder onto an existing assistant message's content.
|
|
118
|
-
* Thin wrapper over prefixContent (behavioral semantics unchanged).
|
|
119
|
-
*/
|
|
120
|
-
function withPlaceholderPrefix(content) {
|
|
121
|
-
return prefixContent(content, COMPACTION_PLACEHOLDER)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* D-CC19 restore-path echo merge (2026-09-16 ENGINE-DEBT 批 ED-1): persisted `contextHistory`
|
|
126
|
-
* is loaded back VERBATIM on session restore, so the D-CC18 pathological shape (an assistant
|
|
127
|
-
* WITHOUT reasoning_content directly followed by another assistant — DeepSeek-family thinking
|
|
128
|
-
* mode rejects the first request with 400) can revive from disk. Pure in-core function: scans
|
|
129
|
-
* only at restore time, never prompts the user, never rewrites the session file. Merge direction
|
|
130
|
-
* matches D-CC18 (the reasoning-less message is absorbed INTO its follower — the follower's
|
|
131
|
-
* tool_calls / reasoning_content / other fields are kept verbatim; texts joined with a blank
|
|
132
|
-
* line). Iterates to a fixed point (chains collapse in full). Copy-on-write: messages may be
|
|
133
|
-
* shared with other lines, so the merged message is always a NEW object; clean input returns
|
|
134
|
-
* the SAME array reference (zero copy).
|
|
135
|
-
*/
|
|
136
|
-
|
|
137
|
-
/** Pair predicate: prev = assistant with no/empty reasoning_content and no tool_calls,
|
|
138
|
-
* directly followed by another assistant. (Prev WITH tool_calls is never merged — pairing
|
|
139
|
-
* safety, F-3.) */
|
|
140
|
-
export function isAssistantEchoPair(prev, next) {
|
|
141
|
-
return prev?.role === "assistant"
|
|
142
|
-
&& next?.role === "assistant"
|
|
143
|
-
&& !prev.reasoning_content
|
|
144
|
-
&& !(Array.isArray(prev.tool_calls) && prev.tool_calls.length > 0)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** Text of a message content in any supported shape (string / parts array / null-ish). */
|
|
148
|
-
function contentTextOf(content) {
|
|
149
|
-
if (typeof content === "string") return content
|
|
150
|
-
if (Array.isArray(content)) {
|
|
151
|
-
return content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("\n\n")
|
|
152
|
-
}
|
|
153
|
-
return ""
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** Absorb prev's text into next's content (blank-line join; empty prev text → next unchanged). */
|
|
157
|
-
function absorbEchoContent(prevContent, nextContent) {
|
|
158
|
-
const text = contentTextOf(prevContent)
|
|
159
|
-
if (text.length === 0) return nextContent
|
|
160
|
-
return prefixContent(nextContent, text)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export function mergeAdjacentAssistantEchoes(history) {
|
|
164
|
-
if (!Array.isArray(history)) return history
|
|
165
|
-
let src = history
|
|
166
|
-
for (;;) {
|
|
167
|
-
let changed = false
|
|
168
|
-
const next = []
|
|
169
|
-
for (let i = 0; i < src.length; i++) {
|
|
170
|
-
if (i + 1 < src.length && isAssistantEchoPair(src[i], src[i + 1])) {
|
|
171
|
-
next.push({ ...src[i + 1], content: absorbEchoContent(src[i].content, src[i + 1].content) })
|
|
172
|
-
i += 1
|
|
173
|
-
changed = true
|
|
174
|
-
} else {
|
|
175
|
-
next.push(src[i])
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (!changed) return src === history ? history : src
|
|
179
|
-
src = next
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
73
|
/** Replace middle with a note, then re-inject task/plan state (shared by LLM summary and truncation fallback) */
|
|
184
74
|
function applyCompression(agent, headEnd, tailStart, note) {
|
|
185
75
|
// _fullHistory already holds every real message (written at the source via pushReal),
|
|
@@ -314,7 +204,7 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
314
204
|
messages,
|
|
315
205
|
tools: extras?.tools,
|
|
316
206
|
signal,
|
|
317
|
-
// §
|
|
207
|
+
// TRACES.md §6.1 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
|
|
318
208
|
// depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
|
|
319
209
|
logCtx: {
|
|
320
210
|
stage: "compress", child: agent._logId, kind: "compress",
|
|
@@ -342,37 +232,6 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
342
232
|
return true
|
|
343
233
|
}
|
|
344
234
|
|
|
345
|
-
/** prune stub 逐字(§6.16.3):只给「已清理 + 原长度 + 重跑路径」——prune 是**删**不是摘要,不声称可复原。 */
|
|
346
|
-
const pruneStub = (chars) => `[pruned: stale tool output dropped (${chars} chars) — re-run the tool if you need it again.]`
|
|
347
|
-
|
|
348
|
-
/**
|
|
349
|
-
* 陈旧工具输出清理(F-CC3 · §6.16.3):合格集 = `token-window.mjs` 单源(保护尾外 ∧ `role:"tool"` ∧ ≥ 门槛);
|
|
350
|
-
* 命中项**原位换「内容」**(`history[i] = { ...m, content: stub }`——数组引用 / 长度 / 索引 / `tool_call_id` 全不变
|
|
351
|
-
* ⇒ 配对**结构上不可能被拆**)。记录面零改(copy-on-write:消息对象与人读线共享);基线失效同 `shrinkOversized` 先例。
|
|
352
|
-
* @returns {{pruned:number, freed:number, candidates:number, tailKept:number, belowMin:number}}
|
|
353
|
-
*/
|
|
354
|
-
export function pruneStaleToolOutputs(agent) {
|
|
355
|
-
const history = agent.history
|
|
356
|
-
const stale = collectStaleToolOutputs(history, agent.provider)
|
|
357
|
-
const counts = {
|
|
358
|
-
pruned: stale.indexes.length,
|
|
359
|
-
freed: stale.tokens,
|
|
360
|
-
candidates: stale.candidates,
|
|
361
|
-
tailKept: stale.tailKept,
|
|
362
|
-
belowMin: stale.belowMin,
|
|
363
|
-
}
|
|
364
|
-
if (counts.pruned === 0) return counts
|
|
365
|
-
for (const i of stale.indexes) {
|
|
366
|
-
const m = history[i]
|
|
367
|
-
// 多模态 tool 结果(content 数组)整体替换为 stub ⇒ 图像 part 丢弃(不可再取——prune 是删;回执只给重跑路径)
|
|
368
|
-
const chars = typeof m.content === "string" ? m.content.length : JSON.stringify(m.content ?? "").length
|
|
369
|
-
history[i] = { ...m, content: pruneStub(chars) }
|
|
370
|
-
}
|
|
371
|
-
agent._lastPromptTokens = null
|
|
372
|
-
agent._usageAtLen = null
|
|
373
|
-
return counts
|
|
374
|
-
}
|
|
375
|
-
|
|
376
235
|
/**
|
|
377
236
|
* Deterministic truncation fallback: called when the summary LLM fails repeatedly, no network call.
|
|
378
237
|
* Drops the middle so the task can continue. Returns whether truncation happened.
|
|
@@ -389,50 +248,12 @@ export function compressFallback(agent) {
|
|
|
389
248
|
return true
|
|
390
249
|
}
|
|
391
250
|
|
|
392
|
-
/** Hard truncation limit for a single message body: when exceeded and the splitter can't find a middle section, truncate to a stub (prevents one giant message from blocking compaction) */
|
|
393
|
-
const OVERSIZE_CONTENT_LIMIT = 8_000
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Deterministic shrinking: last resort when splitHistory can't find a middle section (history too short) but threshold is exceeded. No LLM call.
|
|
397
|
-
* Truncates user/tool message bodies exceeding OVERSIZE_CONTENT_LIMIT to a stub (keeps head + tail);
|
|
398
|
-
* does not touch reasoning_content (DeepSeek/Kimi echo protocol) or tool_calls pairing structure — no protocol 400
|
|
399
|
-
* risk from this path (this module's echo-safety face is `applyCompression`: the compaction placeholder is never
|
|
400
|
-
* committed as a separate reasoning-less assistant next to another assistant — D-CC18, 2026-09-16 batch).
|
|
401
|
-
* Only called after compressIfNeeded determines threshold is exceeded. Returns whether any message was truncated.
|
|
402
|
-
*/
|
|
403
|
-
function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
404
|
-
let shrunk = false
|
|
405
|
-
// Copy-on-write: build a NEW array and replace only truncated entries. pushReal stores the SAME
|
|
406
|
-
// message object in both `agent.history` (machine line) and `agent._fullHistory` (human/persistence
|
|
407
|
-
// line), so in-place `m.content = ...` would ALSO truncate the never-compacted human line and lose
|
|
408
|
-
// the original pasted content on session persist (session.mjs persists _fullHistory). VS Code port
|
|
409
|
-
// already copies (`history.map(m => ({ ...m }))`); this brings CLI to parity.
|
|
410
|
-
const next = agent.history.map((m) => {
|
|
411
|
-
if ((m.role !== "user" && m.role !== "tool") || typeof m.content !== "string") return m
|
|
412
|
-
if (m.content.length <= limit) return m
|
|
413
|
-
// Truncate keeping head + tail, insert stub in between; keepHead/keepTail proportional but not exceeding 50%/25% of limit
|
|
414
|
-
const keepHead = Math.min(Math.floor(limit * 0.5), 4000)
|
|
415
|
-
const keepTail = Math.min(Math.floor(limit * 0.25), 2000)
|
|
416
|
-
shrunk = true
|
|
417
|
-
return {
|
|
418
|
-
...m,
|
|
419
|
-
content:
|
|
420
|
-
m.content.slice(0, keepHead) +
|
|
421
|
-
`\n[... ${m.content.length - keepHead - keepTail} chars truncated — single message too large for context window ...]\n` +
|
|
422
|
-
m.content.slice(-keepTail),
|
|
423
|
-
}
|
|
424
|
-
})
|
|
425
|
-
if (shrunk) {
|
|
426
|
-
agent.history = next
|
|
427
|
-
// Same as compaction: measured token baseline is invalidated by the changed history, fall back to estimation until next response
|
|
428
|
-
agent._lastPromptTokens = null
|
|
429
|
-
agent._usageAtLen = null
|
|
430
|
-
}
|
|
431
|
-
return shrunk
|
|
432
|
-
}
|
|
433
|
-
|
|
434
251
|
// ─── 迁出面(import 面保持:TUI / verify-compress / VSC 对拍经本档取——先例 = 下方 explore-distill 再导出)──
|
|
435
252
|
export { estimateTokens }
|
|
253
|
+
// ─── 拆分转口(2026-10-01 拆分批 · #755 ∥ #786):写缝 ∥ 回声 ∥ 降级三面外提——消费面 / 批内件 import 面零改 ───
|
|
254
|
+
export { pushReal, pushRecord } from "./context-push.mjs"
|
|
255
|
+
export { isAssistantEchoPair, mergeAdjacentAssistantEchoes } from "./context-echo.mjs"
|
|
256
|
+
export { pruneStaleToolOutputs } from "./context-degrade.mjs"
|
|
436
257
|
// ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
|
|
437
258
|
// 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
|
|
438
259
|
// explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
|