thincoder 0.12.58 → 0.12.60
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 +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* crash-reports.mjs — R25(docs/design/ARCHITECTURE.md §R25——F-R25a/b/c)CLI 异常终止
|
|
3
|
+
* 捕获与留痕机制。
|
|
4
|
+
*
|
|
5
|
+
* 三类能力(与设计逐项对应):
|
|
6
|
+
* - prepareCrashReporting()(F-R25b):入口最前调用——mkdir 预建 ~/.thincoder/crash-reports/
|
|
7
|
+
* + process.report 代码内启用(reportOnFatalError + directory)——V8 OOM/原生 fatal
|
|
8
|
+
* 自动写 report.*.json。实现批实测(2026-09-07):目录缺失时 Node 对 fatal 静默不写
|
|
9
|
+
* 报告——预建是必要动作而非"零成本保险"。
|
|
10
|
+
* - writeCrashRecord()(F-R25a):JS 异常钩子同步落盘 crash-{ts}-{pid}.json(ts = epoch ms
|
|
11
|
+
* UTC + pid——跨进程同 ms 防覆盖——复审 #3)——权限 0600(config.json 先例)。
|
|
12
|
+
* - recentCrashHint()(F-R25c):启动扫描 24h 窗内记录(两类文件模式定死——评审 #8:
|
|
13
|
+
* crash-*.json 自写 + report.*.json Node fatal——mtime 判定)→ 返回提示文本或 null
|
|
14
|
+
* (无匹配不提示——负例)。
|
|
15
|
+
*
|
|
16
|
+
* 保留策略(评审 #6 + 复审 #4):>30 天淘汰——写时自清理(tool-results 先例『落盘目录
|
|
17
|
+
* 写时自清理』段同动机——Windows 磁盘清理不覆盖 ~/.thincoder)——清理搭车点 = F-R25a 写 +
|
|
18
|
+
* F-R25b 入口 mkdir + F-R25c 启动扫描(纯 fatal 序列 JS 不运行——下个进程入口 mkdir 时
|
|
19
|
+
* 清理)。
|
|
20
|
+
*
|
|
21
|
+
* 纪律:全部同步 API(崩溃路径无异步);调用方按执行序列各步独立 try/catch(复审 #2——
|
|
22
|
+
* 写失败/恢复失败不阻断后续步——exit 非 0 恒达)。
|
|
23
|
+
*/
|
|
24
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"
|
|
25
|
+
import { join } from "node:path"
|
|
26
|
+
import { configDir } from "./config.mjs"
|
|
27
|
+
|
|
28
|
+
/** crash-reports 运行时目录(~/.thincoder/crash-reports——非仓内——写时自清理)。 */
|
|
29
|
+
export function crashReportsDir() {
|
|
30
|
+
return join(configDir, "crash-reports")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 记录文件模式(评审 #8 定死):crash-*.json = 自写(F-R25a);report.*.json = Node
|
|
34
|
+
* fatal(默认命名 report.YYYYMMDD.HHMMSS.<pid>.<seq>.json——2026-09-07 实现批实测)。 */
|
|
35
|
+
function isCrashRecordName(name) {
|
|
36
|
+
return /^crash-.+\.json$/.test(name) || /^report\..+\.json$/.test(name)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** >30 天淘汰(评审 #6)——写时自清理;搭车点 = 写 / 入口 mkdir / 启动扫描(复审 #4)。 */
|
|
40
|
+
function purgeOldCrashReports(dir) {
|
|
41
|
+
const cutoff = Date.now() - 30 * 24 * 3_600_000
|
|
42
|
+
let names
|
|
43
|
+
try { names = readdirSync(dir) } catch { return } // 目录不存在/不可读 → 无事可做
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
if (!isCrashRecordName(name)) continue
|
|
46
|
+
try {
|
|
47
|
+
if (statSync(join(dir, name)).mtimeMs < cutoff) unlinkSync(join(dir, name))
|
|
48
|
+
} catch { /* 单个文件失败不影响其余 */ }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* F-R25b:入口最前调用(一切重活前——缩编程期窗口)——预建目录 + process.report 启用。
|
|
54
|
+
* 任何失败不阻断启动(尽力面——record 路径自带降级)。返回目录路径。
|
|
55
|
+
*/
|
|
56
|
+
export function prepareCrashReporting() {
|
|
57
|
+
const dir = crashReportsDir()
|
|
58
|
+
try {
|
|
59
|
+
mkdirSync(dir, { recursive: true })
|
|
60
|
+
// 代码内启用:shebang 入口无法携带启动参数(env 单参数限制 + execArgv 仅子进程——评审 #1)
|
|
61
|
+
process.report.directory = dir
|
|
62
|
+
process.report.reportOnFatalError = true
|
|
63
|
+
} catch { /* mkdir/启用失败不阻断启动——尽力面 */ }
|
|
64
|
+
purgeOldCrashReports(dir) // F-R25b 入口 mkdir 搭车清理(复审 #4——纯 fatal 序列也触发)
|
|
65
|
+
return dir
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* F-R25a:JS 异常钩子同步写诊断记录(内容:时间/类型/错误消息+堆栈/uptime/argv/cwd/
|
|
70
|
+
* 内存/node+版本——设计字段集)。
|
|
71
|
+
* @param {string} type uncaughtException | unhandledRejection
|
|
72
|
+
* @param {*} error 原始 error(unhandledRejection 可 reject 任意值——非 Error 兜底 String)
|
|
73
|
+
* @returns {string|null} 记录完整路径;写失败 → null(不抛——调用方步隔离——复审 #2)
|
|
74
|
+
*/
|
|
75
|
+
export function writeCrashRecord({ type, error }) {
|
|
76
|
+
const dir = crashReportsDir()
|
|
77
|
+
try {
|
|
78
|
+
const record = {
|
|
79
|
+
time: new Date().toISOString(),
|
|
80
|
+
type,
|
|
81
|
+
message: error?.message ?? String(error),
|
|
82
|
+
stack: typeof error?.stack === "string" ? error.stack : null,
|
|
83
|
+
uptime: process.uptime(),
|
|
84
|
+
argv: process.argv,
|
|
85
|
+
cwd: process.cwd(),
|
|
86
|
+
memoryUsage: process.memoryUsage(),
|
|
87
|
+
node: process.version,
|
|
88
|
+
}
|
|
89
|
+
// ts = epoch ms UTC + pid——跨进程同 ms 防覆盖(复审 #3)
|
|
90
|
+
const file = join(dir, `crash-${Date.now()}-${process.pid}.json`)
|
|
91
|
+
writeFileSync(file, JSON.stringify(record, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
92
|
+
try { chmodSync(file, 0o600) } catch { /* Windows chmod 尽力而为——config.json 先例 */ }
|
|
93
|
+
purgeOldCrashReports(dir) // F-R25a 写时搭车清理(评审 #6)
|
|
94
|
+
return file
|
|
95
|
+
} catch {
|
|
96
|
+
return null // 写失败不阻断后续步
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* F-R25c:启动扫描——crash-reports 24h 窗内是否有记录(两类模式——mtime 判定)。
|
|
102
|
+
* @param {object} [opts] 可选 { dir }——目录注入(测试用);默认 crashReportsDir()
|
|
103
|
+
* @returns {string|null} 提示文本(含最新记录完整路径);无匹配 → null(负例——不提示)
|
|
104
|
+
*/
|
|
105
|
+
export function recentCrashHint({ dir = crashReportsDir() } = {}) {
|
|
106
|
+
purgeOldCrashReports(dir) // F-R25c 启动扫描搭车清理(复审 #4)
|
|
107
|
+
if (!existsSync(dir)) return null
|
|
108
|
+
const cutoff = Date.now() - 24 * 3_600_000
|
|
109
|
+
let best = null // 窗内 mtime 最新记录
|
|
110
|
+
let names
|
|
111
|
+
try { names = readdirSync(dir) } catch { return null } // 目录不可读 → 静默无提示
|
|
112
|
+
for (const name of names) {
|
|
113
|
+
if (!isCrashRecordName(name)) continue
|
|
114
|
+
try {
|
|
115
|
+
const st = statSync(join(dir, name))
|
|
116
|
+
if (st.mtimeMs >= cutoff && (!best || st.mtimeMs > best.mtimeMs)) {
|
|
117
|
+
best = { path: join(dir, name), mtimeMs: st.mtimeMs }
|
|
118
|
+
}
|
|
119
|
+
} catch { /* 单文件 stat 失败跳过 */ }
|
|
120
|
+
}
|
|
121
|
+
if (!best) return null
|
|
122
|
+
return `上次运行异常终止(记录:${best.path})`
|
|
123
|
+
}
|
package/src/distill.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { chat } from "./provider/index.mjs"
|
|
8
|
+
import { loadConfig } from "./config.mjs"
|
|
8
9
|
import { put, putMarkdown } from "./memory.mjs"
|
|
9
10
|
import { commitAndPush } from "./git/gitmem.mjs"
|
|
10
11
|
|
|
@@ -17,7 +18,7 @@ Output a JSON array (nothing else):
|
|
|
17
18
|
"title": "Short title",
|
|
18
19
|
"content": "Full content, self-contained — understandable without session context",
|
|
19
20
|
"tags": ["tag1", "tag2"],
|
|
20
|
-
"
|
|
21
|
+
"layer": "personal | project"
|
|
21
22
|
}
|
|
22
23
|
]
|
|
23
24
|
|
|
@@ -26,7 +27,7 @@ Extraction criteria:
|
|
|
26
27
|
- decision: technical decisions made in the session and their rationale
|
|
27
28
|
- pattern: debugging experiences, solutions, reusable workflows
|
|
28
29
|
- rule: coding standards (caution! rules are usually best written manually; only extract rules explicitly established in the session)
|
|
29
|
-
-
|
|
30
|
+
- layer: use "project" for project-specific knowledge; use "personal" for general or personal preferences
|
|
30
31
|
|
|
31
32
|
Do NOT extract:
|
|
32
33
|
- one-off task details ("changed line X in file Y today")
|
|
@@ -39,13 +40,30 @@ If the session is long, prioritize conclusions that appeared last and are still
|
|
|
39
40
|
Session log:
|
|
40
41
|
`
|
|
41
42
|
|
|
43
|
+
/** §18.6 D-TR6(2026-09-04 fix round1):distill 调用点无 agent 作用域——traces 开关
|
|
44
|
+
* 缺省回退磁盘配置(loadConfig——与 agent.config 同源:traces.enabled 缺省 on);
|
|
45
|
+
* 配置不可读时按缺省 on(注:CLI 启动早已 loadConfig——此处仅是兜底防御)。 */
|
|
46
|
+
function tracesEnabledFromConfig() {
|
|
47
|
+
try {
|
|
48
|
+
return loadConfig().traces?.enabled !== false
|
|
49
|
+
} catch {
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/**
|
|
43
55
|
* Extract candidates from a session transcript. transcript: plain-text session record.
|
|
44
|
-
* Returns [{ type, title, content, tags,
|
|
56
|
+
* Returns [{ type, title, content, tags, layer }], or [] on parse failure.
|
|
57
|
+
* opts.traces(可选):§18.6 D-TR6 开关显式透传(测试隔离/未来调用方)——缺省回退
|
|
58
|
+
* 磁盘配置(tracesEnabledFromConfig)——关 = chat() 出口不落盘。
|
|
45
59
|
*/
|
|
46
|
-
export async function extractCandidates(provider, transcript) {
|
|
60
|
+
export async function extractCandidates(provider, transcript, opts = {}) {
|
|
61
|
+
const traces = opts.traces ?? tracesEnabledFromConfig()
|
|
47
62
|
const res = await chat(provider, {
|
|
48
63
|
messages: [{ role: "user", content: DISTILL_PROMPT + transcript }],
|
|
64
|
+
// §18.6 D-TR4/D-TR6(fix round1):distill 调用经 chat() 唯一采集点——补轨迹
|
|
65
|
+
// 元数据 + traces 开关透传(关=不落盘必须全覆盖——不再静默越过开关)
|
|
66
|
+
logCtx: { stage: "distill", kind: "distill", traces },
|
|
49
67
|
})
|
|
50
68
|
// Balanced-bracket extraction: find the first '[' and track depth through nested
|
|
51
69
|
// brackets (tags arrays, nested objects, etc.) until the matching ']'.
|
|
@@ -99,23 +117,23 @@ export function historyToTranscript(history, { maxChars = 30_000 } = {}) {
|
|
|
99
117
|
/**
|
|
100
118
|
* Write confirmed candidates to the specified layer.
|
|
101
119
|
* opts: { projectDir, team: { dir } | null, author }
|
|
102
|
-
*
|
|
120
|
+
* layer=team requires opts.team; project requires opts.projectDir.
|
|
103
121
|
* Returns write result description.
|
|
104
122
|
*/
|
|
105
123
|
export async function saveCandidate(memory, candidate, opts = {}) {
|
|
106
|
-
const
|
|
124
|
+
const layer = candidate.layer ?? candidate.scope ?? "personal"
|
|
107
125
|
// tags come from LLM output (untrusted): if not an array, stringify then split by comma/whitespace —
|
|
108
126
|
// calling .split on a non-string would crash, and models often produce "a, b" comma strings
|
|
109
127
|
const tags = Array.isArray(candidate.tags)
|
|
110
128
|
? candidate.tags.map((t) => String(t)).filter(Boolean)
|
|
111
129
|
: String(candidate.tags ?? "").split(/[\s,]+/).filter(Boolean)
|
|
112
130
|
|
|
113
|
-
if (
|
|
131
|
+
if (layer === "personal") {
|
|
114
132
|
const id = await put(memory, { type: candidate.type, title: candidate.title, content: candidate.content, tags: tags.join(" ") })
|
|
115
133
|
return `personal#${id}`
|
|
116
134
|
}
|
|
117
|
-
if (
|
|
118
|
-
if (!opts.projectDir) throw new Error("project
|
|
135
|
+
if (layer === "project") {
|
|
136
|
+
if (!opts.projectDir) throw new Error("project layer unavailable — no project directory configured (set memory.projectDir in ~/.thincoder/config.json)")
|
|
119
137
|
const filename = await putMarkdown(memory, {
|
|
120
138
|
layer: "project", dir: opts.projectDir,
|
|
121
139
|
type: candidate.type, title: candidate.title, content: candidate.content,
|
|
@@ -123,8 +141,8 @@ export async function saveCandidate(memory, candidate, opts = {}) {
|
|
|
123
141
|
})
|
|
124
142
|
return `project:${filename}`
|
|
125
143
|
}
|
|
126
|
-
if (
|
|
127
|
-
if (!opts.team?.dir) throw new Error("team
|
|
144
|
+
if (layer === "team") {
|
|
145
|
+
if (!opts.team?.dir) throw new Error("team layer not configured — configure memory.team in ~/.thincoder/config.json")
|
|
128
146
|
const filename = await putMarkdown(memory, {
|
|
129
147
|
layer: "team", dir: opts.team.dir,
|
|
130
148
|
type: candidate.type, title: candidate.title, content: candidate.content,
|
|
@@ -133,5 +151,5 @@ export async function saveCandidate(memory, candidate, opts = {}) {
|
|
|
133
151
|
await commitAndPush(opts.team.dir, filename, `memory: [${candidate.type}] ${candidate.title} (distilled)`)
|
|
134
152
|
return `team:${filename}`
|
|
135
153
|
}
|
|
136
|
-
throw new Error(`unknown
|
|
154
|
+
throw new Error(`unknown layer: ${layer}`)
|
|
137
155
|
}
|
package/src/escape.mjs
CHANGED
|
@@ -131,13 +131,15 @@ export function escapeMessageContent(message) {
|
|
|
131
131
|
return changed ? next : message
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** IKBGX4
|
|
134
|
+
/** IKBGX4 + SESSION.md §9 D-S1:剥离仅本地使用的整消息标记字段(transient/ts)——发送给 provider 前移除。
|
|
135
135
|
* 严格 OpenAI 兼容服务端(opencode/LiteLLM 等)会拒绝消息级未知 key
|
|
136
|
-
* ("Extra inputs are not permitted, field: 'messages[i].transient'"
|
|
136
|
+
* ("Extra inputs are not permitted, field: 'messages[i].transient'");ts 同理
|
|
137
|
+
* (消息时间戳是本地取证字段,不进任何 provider 请求——T-S3)。copy-on-write:
|
|
138
|
+
* 历史里的原对象不动(read_history 仍能读到 ts)。 */
|
|
137
139
|
export function stripLocalMessageFields(messages) {
|
|
138
140
|
return messages.map((m) => {
|
|
139
|
-
if (m && typeof m === "object" && "transient" in m) {
|
|
140
|
-
const { transient, ...rest } = m
|
|
141
|
+
if (m && typeof m === "object" && ("transient" in m || "ts" in m)) {
|
|
142
|
+
const { transient, ts, ...rest } = m
|
|
141
143
|
return rest
|
|
142
144
|
}
|
|
143
145
|
return m
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explore-distill.mjs — End-of-run exploration distillation (AGENT-LOOP §13 +
|
|
3
|
+
* CONTEXT-COMPACTION §5, 2026-08-23). 2026-09-05 module-split: moved verbatim out of
|
|
4
|
+
* context.mjs (524 > 500 hard limit). The main agent's machine line is flooded by inline
|
|
5
|
+
* step-by-step exploration (read/grep/...). At run end we distill THIS run's exploration
|
|
6
|
+
* tool-results into one semantic summary note that replaces them in the machine line,
|
|
7
|
+
* while agent._fullHistory (the human line) stays untouched. VS Code compact.mjs
|
|
8
|
+
* distillation mirrors this module (same-name file in thincoder-vscode/src, cross-repo
|
|
9
|
+
* parity anchors point here).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { chat } from "./provider/index.mjs"
|
|
13
|
+
|
|
14
|
+
/** Read-only knowledge tools counted as "exploration" (execute writes files → never exploration). */
|
|
15
|
+
export const EXPLORE_TOOLS = new Set([
|
|
16
|
+
"read", "grep", "glob", "ls", "code_search", "doc_search", "repo_outline",
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/** Summary prompt for turning a burst of exploration results into a semantic summary. */
|
|
20
|
+
export const EXPLORE_SUMMARY_PROMPT = `You are distilling exploration tool results. Summarize the following read-only codebase exploration into a compact semantic summary for the main agent's own context.
|
|
21
|
+
|
|
22
|
+
Requirements:
|
|
23
|
+
- Capture WHAT was discovered, WHERE (which files / directories / symbols), and the KEY CONCLUSIONS — do not list tool calls mechanically
|
|
24
|
+
- Keep actionable facts the main agent needs to continue: code locations, function names, file paths, structure, and open questions the exploration raised
|
|
25
|
+
- Drop raw tool-output noise, repeated lines, and verbatim file dumps — keep only what must be remembered
|
|
26
|
+
- Be honest: mark anything not actually verified as "unverified"; do not present guesses as facts
|
|
27
|
+
- Use bullet points; aim for information completeness, not a hard word limit
|
|
28
|
+
|
|
29
|
+
Exploration log:
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
/** tool_calls name across both stored shapes ({function:{name}} and flat {name}). */
|
|
33
|
+
function toolCallName(tc) {
|
|
34
|
+
return tc?.function?.name ?? tc?.name ?? ""
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Tool that produced a tool-result message (falls back to its owner assistant's tool_call). */
|
|
38
|
+
function toolResultName(msg, ownerToolCalls) {
|
|
39
|
+
if (typeof msg?.name === "string" && msg.name) return msg.name
|
|
40
|
+
const owner = (ownerToolCalls ?? []).find((tc) => tc.id === msg?.tool_call_id)
|
|
41
|
+
return owner ? toolCallName(owner) : ""
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find the pure-exploration "assistant(tool_calls)→tool…" pair blocks added since `start`.
|
|
46
|
+
* A block is explorable only when EVERY tool call AND every tool result in it is an exploration
|
|
47
|
+
* tool — mixed blocks (read + edit in one turn) stay untouched, or we'd orphan the edit pairing.
|
|
48
|
+
*/
|
|
49
|
+
function findExplorationBlocks(history, start) {
|
|
50
|
+
const blocks = []
|
|
51
|
+
let i = start
|
|
52
|
+
while (i < history.length) {
|
|
53
|
+
const m = history[i]
|
|
54
|
+
if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
55
|
+
let j = i + 1
|
|
56
|
+
while (j < history.length && history[j]?.role === "tool") j++
|
|
57
|
+
const toolMsgs = history.slice(i + 1, j)
|
|
58
|
+
const allCallsExplore = m.tool_calls.every((tc) => EXPLORE_TOOLS.has(toolCallName(tc)))
|
|
59
|
+
const allResultsExplore = toolMsgs.length > 0 && toolMsgs.every((t) => EXPLORE_TOOLS.has(toolResultName(t, m.tool_calls)))
|
|
60
|
+
if (allCallsExplore && allResultsExplore) {
|
|
61
|
+
blocks.push({ start: i, end: j, messages: history.slice(i, j), toolCount: toolMsgs.length })
|
|
62
|
+
}
|
|
63
|
+
i = j
|
|
64
|
+
} else {
|
|
65
|
+
i++
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return blocks
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Serialize a batch of exploration messages for the summary LLM (same shape as compaction serialization). */
|
|
72
|
+
function serializeExplorationMessages(messages) {
|
|
73
|
+
const cap = 8000 // exploration results ARE the signal to distill — generous cap (quality-first, N1)
|
|
74
|
+
return messages
|
|
75
|
+
.map((m) => {
|
|
76
|
+
const toolNote = m.tool_calls ? ` [called tools: ${m.tool_calls.map(toolCallName).join(", ")}]` : ""
|
|
77
|
+
let text = ""
|
|
78
|
+
if (typeof m.content === "string") text = m.content
|
|
79
|
+
else if (Array.isArray(m.content)) text = m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join(" ")
|
|
80
|
+
return `[${m.role}]${toolNote} ${text.slice(0, cap)}`
|
|
81
|
+
})
|
|
82
|
+
.join("\n")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Core (shared) distillation: replace this run's pure-exploration pair blocks with a single
|
|
87
|
+
* "[Exploration summary]" note placed where the first block was. Returns a NEW history array,
|
|
88
|
+
* or null when there is nothing to shrink (<3 exploration results / LLM failure). Pairing-safe:
|
|
89
|
+
* whole assistant→tool blocks are removed, so no orphan tool_calls/tool can survive.
|
|
90
|
+
*/
|
|
91
|
+
async function distillExplorations(history, start, provider, signal, agent, depth) {
|
|
92
|
+
if (!Array.isArray(history) || history.length - start < 2) return null
|
|
93
|
+
const blocks = findExplorationBlocks(history, start)
|
|
94
|
+
const resultCount = blocks.reduce((n, b) => n + b.toolCount, 0)
|
|
95
|
+
if (resultCount < 3) return null
|
|
96
|
+
|
|
97
|
+
const serialized = blocks.map((b) => serializeExplorationMessages(b.messages)).join("\n")
|
|
98
|
+
|
|
99
|
+
let summary
|
|
100
|
+
try {
|
|
101
|
+
// Silent by design (D11): thinking:null and no onToken/onReasoning — this internal
|
|
102
|
+
// distillation must not stream to the frontend. signal propagates user cancellation.
|
|
103
|
+
const resp = await chat({ ...provider, thinking: null, reasoningEffort: null }, {
|
|
104
|
+
messages: [{ role: "user", content: EXPLORE_SUMMARY_PROMPT + serialized }],
|
|
105
|
+
signal,
|
|
106
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
|
|
107
|
+
// depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
|
|
108
|
+
logCtx: {
|
|
109
|
+
stage: "distill", child: agent?._logId ?? null, kind: "distill",
|
|
110
|
+
role: agent?._role ?? null, depth: depth ?? null,
|
|
111
|
+
session: agent?._sessionStart ?? null, cwd: agent?.cwd ?? process.cwd(),
|
|
112
|
+
traces: agent?.config?.traces?.enabled !== false,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
summary = resp?.content
|
|
116
|
+
} catch {
|
|
117
|
+
return null // N3: never block the run's return or lose history — original results stay
|
|
118
|
+
}
|
|
119
|
+
if (!summary) return null
|
|
120
|
+
|
|
121
|
+
const drop = new Set()
|
|
122
|
+
for (const b of blocks) for (let k = b.start; k < b.end; k++) drop.add(k)
|
|
123
|
+
const note = { role: "user", content: "[Exploration summary]\n" + summary }
|
|
124
|
+
const next = []
|
|
125
|
+
let inserted = false
|
|
126
|
+
for (let k = 0; k < history.length; k++) {
|
|
127
|
+
if (drop.has(k)) {
|
|
128
|
+
if (!inserted) { next.push(note); inserted = true }
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
next.push(history[k])
|
|
132
|
+
}
|
|
133
|
+
return next
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* End-of-run exploration distillation (runAgent's final return). Shrinks the MACHINE line
|
|
138
|
+
* (agent.history) only; agent._fullHistory is never touched. Triggers when this run added ≥3
|
|
139
|
+
* exploration tool results; on LLM failure it silently keeps the original history (N3).
|
|
140
|
+
* The distillation itself is silent and never streams (D11); `callbacks.onDistilled` fires
|
|
141
|
+
* ONLY after the replacement actually lands (never on no-op/failure) — callers persist the
|
|
142
|
+
* compressed session (SEND-STALL-DISTILL §2.3).
|
|
143
|
+
*/
|
|
144
|
+
export async function summarizeRunExplorations(agent, callbacks, signal, depth = 0) {
|
|
145
|
+
const next = await distillExplorations(agent.history, agent._runStartHistoryLen ?? 0, agent.provider, signal, agent, depth)
|
|
146
|
+
if (!next) return
|
|
147
|
+
agent.history = next
|
|
148
|
+
// The machine line changed shape — the measured token baseline was for the pre-shrink context.
|
|
149
|
+
// Invalidate so the next compaction check re-estimates instead of over-counting stale history.
|
|
150
|
+
agent._lastPromptTokens = null
|
|
151
|
+
agent._usageAtLen = null
|
|
152
|
+
// The compressed machine line must reach the disk: the run's own save already happened,
|
|
153
|
+
// so without this hook the async distill would leave the session un-compressed on exit.
|
|
154
|
+
callbacks.onDistilled?.()
|
|
155
|
+
}
|
package/src/log.mjs
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* log.mjs — 诊断事件日志(docs/design/LOGGING.md 权威规格)。
|
|
3
|
+
* 双端同构:thincoder/src/log.mjs 与 thincoder-vscode/src/log.mjs 同一实现语义
|
|
4
|
+
* (共享 ~/.thincoder/logs/、同格式、同事件面——F-L6)。
|
|
5
|
+
*
|
|
6
|
+
* 常驻骨架日志:回合/LLM/工具/子代理/挂起的关键事件——每行一个 JSON 事件,按天轮转
|
|
7
|
+
* (agent-YYYY-MM-DD.log),保留 1 天——用户零手动维护(F-L3/NF-L4)。问题发生时
|
|
8
|
+
* 直接 tail/grep 定位断点(根治"临时插桩再删"循环)。
|
|
9
|
+
*
|
|
10
|
+
* 纪律(实现侧逐条落实):
|
|
11
|
+
* - fire-and-forget:logEvent 失败静默降级(NF-L1)——主流程零影响。进程内首次写失败
|
|
12
|
+
* 即置死(_dead),当日不再尝试(磁盘满/权限错不逐事件空转)。
|
|
13
|
+
* - 单事件行 <512 字符(NF-L2):head(LLM ≤300 / 工具 ≤200)、err ≤200 由调用方截断,
|
|
14
|
+
* 本模块对任意字符串字段做上限兜底(head 300 / err 200 / 其余 120)+ 超长时丢可选字段。
|
|
15
|
+
* - 敏感字段零落盘(NF-L3/§2.4,2026-09-03 评审 refinement #7 匹配语义定稿):
|
|
16
|
+
* ① 字段名精确匹配黑名单(apiKey/designToken/password/secret/token,大小写不敏感;
|
|
17
|
+
* 另加防御性名称 authorization/proxy/proxyUri——防凭据型 URL/头字段泄漏——§2.5)
|
|
18
|
+
* → 整个字段丢弃;
|
|
19
|
+
* ② 内容只扫密钥形态(sk-xxx / Bearer xxx / key=… 等)→ 截断到形态之前
|
|
20
|
+
* ——宁可丢信息不漏密钥。
|
|
21
|
+
* 工具事件不记 args;URL 不入事件(llm/tool 事件从不携带 URL;err 文本经形态扫描)。
|
|
22
|
+
* - 摘要截断(B 方案,2026-09-03 用户裁定):截断处带 "…" 标记(截后仍 ≤上限)。
|
|
23
|
+
* - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
|
|
24
|
+
* 诊断日志(两端测试套件都会跑真实 agent 管线);显式设置 THINCODER_LOG_DIR 强制
|
|
25
|
+
* 写入该目录(log.test.mjs 用它隔离临时目录——refinement #6)。
|
|
26
|
+
* - 轮转清理(refinement #3——长驻进程覆盖):非仅启动时——每进程每日**首次写事件**
|
|
27
|
+
* 时顺带清理 >1 天的 agent-*.log(extension host 可长驻数月,启动清理覆盖不到)。
|
|
28
|
+
* - seq:每进程单调计数器。双端同写一个文件时 seq 会各自重复——定位同文件时序以
|
|
29
|
+
* ts 为准,seq 仅进程内参照(refinement #8)。
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { appendFileSync, mkdirSync, readdirSync, rmSync, existsSync, statSync } from "node:fs"
|
|
33
|
+
import { homedir } from "node:os"
|
|
34
|
+
import { join } from "node:path"
|
|
35
|
+
|
|
36
|
+
/** 单行长度硬上限(NF-L2) */
|
|
37
|
+
export const MAX_LINE = 512
|
|
38
|
+
/** 字段名精确黑名单(NF-L3/§2.4——2026-09-03 评审 refinement #7:与 §2.4 词表一致含 token) */
|
|
39
|
+
const BLACKLIST_FIELDS = new Set(["apikey", "designtoken", "password", "secret", "token", "authorization", "proxyuri", "proxy"])
|
|
40
|
+
/** 内容密钥形态扫描(大小写不敏感)——命中即截断到形态前 */
|
|
41
|
+
const SECRET_FORM = /(\bsk-[A-Za-z0-9_-]{6,}|\bBearer\s+[A-Za-z0-9._~+/=-]{6,}|\b(?:api[_-]?key|key|token|secret|password|pwd|passwd)\s*[=:]\s*['"]?[A-Za-z0-9._~+/=-]{6,})/i
|
|
42
|
+
/** 字符串字段长度兜底:head ≤300 / err ≤200 / 其余 ≤120 */
|
|
43
|
+
const FIELD_CAPS = { head: 300, err: 200 }
|
|
44
|
+
|
|
45
|
+
let _seq = 0
|
|
46
|
+
let _dead = false // 进程内写失败即死(NF-L1 静默降级——不逐事件重复空转)
|
|
47
|
+
let _cleanupDate = null // 本进程已执行过清理的日期(每日首次写时清一次)
|
|
48
|
+
|
|
49
|
+
/** 日志目录:THINCODER_LOG_DIR(测试隔离/override)> ~/.thincoder/logs(与 sessions/ 同域) */
|
|
50
|
+
export function logsDir() {
|
|
51
|
+
return process.env.THINCODER_LOG_DIR ?? join(homedir(), ".thincoder", "logs")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 今日日志文件路径(agent-YYYY-MM-DD.log) */
|
|
55
|
+
export function todayLogPath(now = new Date()) {
|
|
56
|
+
const ymd = now.toISOString().slice(0, 10)
|
|
57
|
+
return join(logsDir(), `agent-${ymd}.log`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
|
|
61
|
+
* THINCODER_LOG_DIR override(log.test.mjs 隔离临时目录——refinement #6)。 */
|
|
62
|
+
function writeEnabled() {
|
|
63
|
+
if (_dead) return false
|
|
64
|
+
if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_LOG_DIR) return false
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 清理 >1 天的 agent-*.log(NF-L4——refinement #3:机会式,非仅启动时)。
|
|
69
|
+
* 保留窗口(2026-09-03 code review #1 修正):以文件名日期的**当日结束**为龄基准——
|
|
70
|
+
* 今天+昨天的文件保留(任何事件至少留存 24h、最多 48h),早于昨天的删除。
|
|
71
|
+
* 无法按名解析日期的 agent-*.log 按 mtime 兜底(>1 天未写的删除)。
|
|
72
|
+
* 导出供测试直呼(T-L7/T-L7b)。静默:任何失败都不影响主流程。 */
|
|
73
|
+
export const LOG_RETENTION_MS = 24 * 60 * 60 * 1000
|
|
74
|
+
export function cleanupOldLogs(now = new Date()) {
|
|
75
|
+
const dir = logsDir()
|
|
76
|
+
if (!existsSync(dir)) return
|
|
77
|
+
let names
|
|
78
|
+
try {
|
|
79
|
+
names = readdirSync(dir)
|
|
80
|
+
} catch {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
for (const name of names) {
|
|
84
|
+
if (!name.startsWith("agent-") || !name.endsWith(".log")) continue
|
|
85
|
+
const m = name.match(/^agent-(\d{4}-\d{2}-\d{2})\.log$/)
|
|
86
|
+
const ts = m ? Date.parse(m[1] + "T00:00:00Z") : NaN
|
|
87
|
+
// 文件名日期 → 龄以该日结束(ts+24h)计:事件在删除时至少已留存 24h
|
|
88
|
+
const ageMs = Number.isFinite(ts) ? now - (ts + LOG_RETENTION_MS) : (tryStatAge(dir, name, now) ?? Infinity)
|
|
89
|
+
if (ageMs > LOG_RETENTION_MS) {
|
|
90
|
+
try { rmSync(join(dir, name), { force: true }) } catch { /* 静默 */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function tryStatAge(dir, name, now) {
|
|
96
|
+
try {
|
|
97
|
+
return now - statSync(join(dir, name)).mtimeMs
|
|
98
|
+
} catch {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 写入一条事件(fire-and-forget)。fields 内所有字符串都经黑名单/截断处理。
|
|
105
|
+
* 事件行结构:{"ts":ISO,"ev":kind,"seq":N,...fields}——单行 JSON <512 字符。
|
|
106
|
+
*/
|
|
107
|
+
export function logEvent(kind, fields = {}) {
|
|
108
|
+
if (!writeEnabled()) return
|
|
109
|
+
const now = new Date()
|
|
110
|
+
// 机会式轮转清理:每进程每日首次写事件时执行一次(refinement #3——长驻进程覆盖)
|
|
111
|
+
const ymd = now.toISOString().slice(0, 10)
|
|
112
|
+
if (_cleanupDate !== ymd) {
|
|
113
|
+
_cleanupDate = ymd
|
|
114
|
+
cleanupOldLogs(now)
|
|
115
|
+
}
|
|
116
|
+
const entry = { ts: now.toISOString(), ev: kind, seq: ++_seq }
|
|
117
|
+
for (const [k, v] of Object.entries(fields ?? {})) {
|
|
118
|
+
if (v === undefined || v === null) continue
|
|
119
|
+
// ① 字段名精确黑名单 → 丢弃(token 族永不落盘)
|
|
120
|
+
if (BLACKLIST_FIELDS.has(String(k).toLowerCase())) continue
|
|
121
|
+
if (typeof v === "string") entry[k] = sanitizeString(k, v)
|
|
122
|
+
else if (typeof v === "number" || typeof v === "boolean") entry[k] = v
|
|
123
|
+
}
|
|
124
|
+
let line = JSON.stringify(entry)
|
|
125
|
+
// ② 防御性收尾(NF-L2):字段上限后仍超长 → 丢可选字段(保留 ts/ev/seq)直至 <512
|
|
126
|
+
while (line.length > MAX_LINE && Object.keys(entry).length > 3) {
|
|
127
|
+
for (const k of Object.keys(entry)) {
|
|
128
|
+
if (k !== "ts" && k !== "ev" && k !== "seq") { delete entry[k]; break }
|
|
129
|
+
}
|
|
130
|
+
line = JSON.stringify(entry)
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const dir = logsDir()
|
|
134
|
+
mkdirSync(dir, { recursive: true })
|
|
135
|
+
appendFileSync(join(dir, `agent-${ymd}.log`), line + "\n", "utf8")
|
|
136
|
+
} catch {
|
|
137
|
+
_dead = true // NF-L1:写失败静默降级——主流程零影响
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 共享脱敏(§18.6 D-TR2——trace-store 复用;logEvent 自身保持"丢弃字段"语义,
|
|
142
|
+
* 本函数提供"遮蔽标记"语义——同一字段名黑名单 + 同一 SECRET_FORM 形态扫描,
|
|
143
|
+
* 不发明新遮蔽模式)。字段名命中黑名单 → "[REDACTED](整个字段遮蔽);内容命中
|
|
144
|
+
* 密钥形态 → 截断到形态前 + "[redacted]" 标记(宁可丢信息不漏密钥——§2.4)。 */
|
|
145
|
+
export function redactSecret(fieldKey, value) {
|
|
146
|
+
const s = String(value)
|
|
147
|
+
if (BLACKLIST_FIELDS.has(String(fieldKey).toLowerCase())) return "[REDACTED]"
|
|
148
|
+
const hit = s.match(SECRET_FORM)
|
|
149
|
+
return hit ? s.slice(0, hit.index) + "[redacted]" : s
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 内容级净化:密钥形态截断到形态前(②);超长截断 + "…" 标记。 */
|
|
153
|
+
export function sanitizeString(key, value) {
|
|
154
|
+
let s = String(value)
|
|
155
|
+
// 密钥形态 → 截断到形态前(宁可丢信息不漏密钥——§2.4)
|
|
156
|
+
const hit = s.match(SECRET_FORM)
|
|
157
|
+
if (hit) s = s.slice(0, hit.index)
|
|
158
|
+
const cap = FIELD_CAPS[key] ?? 120
|
|
159
|
+
if (s.length > cap) s = s.slice(0, cap - 1) + "…"
|
|
160
|
+
return s
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** 错误文本提取:message + cause 链首条、压成单行、≤max(默认 200——NF-L2/§2.2) */
|
|
164
|
+
export function errText(err, max = 200) {
|
|
165
|
+
let msg = err?.message ?? String(err ?? "")
|
|
166
|
+
if (!msg && err?.cause) msg = String(err.cause)
|
|
167
|
+
if (err?.cause?.message && !/^LLM API error/.test(msg)) msg += ` (${err.cause.message})`
|
|
168
|
+
msg = msg.replace(/\s*\n\s*/g, " ").trim()
|
|
169
|
+
if (msg.length > max) msg = msg.slice(0, max - 1) + "…"
|
|
170
|
+
return msg
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 错误 kind 分类(llm:error 字段):timeout(网关超时/超时信号)/ abort(用户中止)/
|
|
174
|
+
* error(其余——API/网络/未分类)。signal 为请求中止信号(chat 调用点直传)。 */
|
|
175
|
+
export function classifyErr(err, signal) {
|
|
176
|
+
const msg = String(err?.message ?? "").toLowerCase() + " " + String(err?.cause?.message ?? "").toLowerCase()
|
|
177
|
+
if (/timeout|timed ?out|etimedout|time limit exceeded/i.test(msg)) return "timeout"
|
|
178
|
+
if (err?.name === "AbortError" || signal?.aborted) {
|
|
179
|
+
return signal?.reason?.name === "TimeoutError" ? "timeout" : "abort"
|
|
180
|
+
}
|
|
181
|
+
return "error"
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 文本头截取:前 max 字符、单行化(JSON 单行约束)、截断带 "…" 标记。
|
|
185
|
+
* paragraph=true(llm:done 响应头):取首段(首个空行前)——B 方案"首段文本"语义。 */
|
|
186
|
+
export function headText(text, max, { paragraph = false } = {}) {
|
|
187
|
+
let s = String(text ?? "")
|
|
188
|
+
if (paragraph) {
|
|
189
|
+
const p = s.split(/\n\s*\n/, 1)[0]
|
|
190
|
+
if (p.length < s.length) s = p
|
|
191
|
+
}
|
|
192
|
+
s = s.replace(/\s*\n+\s*/g, " ").trim()
|
|
193
|
+
if (s.length > max) s = s.slice(0, max - 1) + "…"
|
|
194
|
+
return s
|
|
195
|
+
}
|
package/src/memory/code-sync.mjs
CHANGED
|
@@ -313,7 +313,8 @@ export function codeSearchTool(memory) {
|
|
|
313
313
|
return {
|
|
314
314
|
name: "code_search",
|
|
315
315
|
description:
|
|
316
|
-
"Search the project's source code for relevant code. Use this to find functions, classes, or code patterns across the codebase. Supports natural language queries and code snippets. Returns matching code chunks with file paths and line numbers."
|
|
316
|
+
"Search the project's source code for relevant code. Use this to find functions, classes, or code patterns across the codebase. Supports natural language queries and code snippets. Returns matching code chunks with file paths and line numbers. Prefer doc_search for the intended design (design docs, conventions); code_search for the implementation as written. " +
|
|
317
|
+
"For what was said in a session (conversation/chat history), use read_history.",
|
|
317
318
|
parameters: {
|
|
318
319
|
type: "object",
|
|
319
320
|
properties: {
|