thincoder 0.12.54 → 0.12.59
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 +98 -0
- package/README.md +1 -1
- package/bin/thincoder.mjs +25 -3
- package/package.json +3 -7
- package/src/acp/bridge.mjs +132 -26
- package/src/advisor/messages.mjs +38 -3
- package/src/advisor/run.mjs +91 -53
- package/src/advisor.mjs +15 -7
- package/src/agent/dispatch.mjs +156 -39
- package/src/agent/helpers.mjs +46 -4
- package/src/agent/setup.mjs +102 -19
- package/src/agent/spawn-child.mjs +28 -1
- package/src/agent-tools/advisor.mjs +43 -11
- package/src/agent-tools/consult.mjs +37 -6
- package/src/agent-tools/eng.mjs +4 -1
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +160 -0
- package/src/agent-tools/settings.mjs +162 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +432 -0
- package/src/agent-tools/subagent-async.mjs +427 -0
- package/src/agent-tools/subagent-scheduler.mjs +319 -0
- package/src/agent-tools/subagent.mjs +565 -128
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +161 -49
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +182 -81
- package/src/auto-think.mjs +14 -0
- package/src/cli/make-agent.mjs +27 -1
- package/src/cli/memory-command.mjs +28 -7
- package/src/cli/permission.mjs +8 -1
- package/src/config.mjs +125 -8
- package/src/context.mjs +115 -34
- package/src/distill.mjs +19 -1
- package/src/escape.mjs +82 -27
- package/src/log.mjs +195 -0
- package/src/mcp/transport-http.mjs +13 -1
- package/src/mcp.mjs +52 -7
- package/src/memory/code-sync.mjs +1 -1
- package/src/memory/core.mjs +204 -10
- package/src/memory/docs.mjs +197 -62
- package/src/memory.mjs +1 -1
- package/src/model-specs.mjs +38 -1
- package/src/prompts/advisor-design.md +46 -0
- package/src/prompts/advisor-round1.md +49 -2
- package/src/prompts/advisor-round2.md +47 -0
- package/src/prompts/advisor-round3.md +47 -0
- package/src/prompts/coder.md +22 -0
- package/src/prompts/consult-base.md +13 -0
- package/src/prompts/discipline.md +25 -6
- package/src/prompts/eng-coder.md +2 -2
- package/src/prompts/engineering-sub.md +23 -1
- package/src/prompts/engineering.md +157 -50
- package/src/prompts/explore.md +1 -2
- package/src/prompts/main.md +11 -5
- package/src/prompts/methodology-template.md +14 -0
- package/src/prompts/system.md +5 -2
- package/src/provider/anthropic.mjs +7 -5
- package/src/provider/core.mjs +104 -28
- package/src/provider/google.mjs +57 -24
- package/src/provider/normalize.mjs +1 -1
- package/src/provider/rate.mjs +0 -2
- package/src/provider/responses.mjs +8 -13
- package/src/provider/sse.mjs +20 -0
- package/src/session.mjs +15 -0
- package/src/tools/apply_patch.md +5 -1
- package/src/tools/bash.md +3 -3
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +92 -0
- package/src/tools/edit-diff.mjs +265 -0
- package/src/tools/edit.md +11 -6
- package/src/tools/execute.md +8 -8
- package/src/tools/execute.mjs +31 -35
- package/src/tools/file.mjs +26 -114
- package/src/tools/file_ops.md +3 -2
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.md +1 -1
- package/src/tools/git.mjs +8 -16
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -2
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +3 -1
- package/src/tools/linter.mjs +9 -37
- package/src/tools/lsp.md +4 -1
- package/src/tools/patch.mjs +84 -13
- package/src/tools/pdf-parse-text.mjs +497 -0
- package/src/tools/pdf-parse-xref.mjs +499 -0
- package/src/tools/pdf.mjs +155 -0
- package/src/tools/question.md +2 -1
- package/src/tools/read.md +1 -0
- package/src/tools/read_pdf.md +21 -0
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +11 -32
- package/src/tools/system.mjs +6 -21
- package/src/tools/tree.md +2 -1
- 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 +387 -24
- package/src/tui/clipboard.mjs +17 -6
- package/src/tui/cmd-config.mjs +29 -9
- package/src/tui/cmd-eng.mjs +1 -0
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp-form.mjs +197 -0
- package/src/tui/cmd-mcp.mjs +264 -114
- package/src/tui/cmd-think.mjs +1 -1
- package/src/tui/index.mjs +49 -95
- package/src/tui/interaction.mjs +41 -3
- package/src/tui/key-handler.mjs +105 -143
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +41 -1
- package/src/tui/pickers.mjs +73 -7
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +45 -20
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +165 -0
- package/src/tui/render.mjs +4 -4
- package/src/tui/startup.mjs +40 -2
- package/src/tui/subagent-blocks.mjs +404 -111
- package/src/tui/subagent-panel.mjs +88 -13
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-events.mjs +172 -95
- package/src/tui/update-notice.mjs +72 -0
- package/src/tui/wizard.mjs +36 -6
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/tools/exec-prelude.mjs +0 -84
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Read a PDF file and extract its text as plain text — page by page, in reading order (row/column layout, `--- Page N ---` separators). Use this instead of `read` for `.pdf` files: read decodes PDFs as UTF-8 garbage.
|
|
2
|
+
|
|
3
|
+
**Route to read_pdf instead of bash:** `pdftotext`, python `pypdf`/`pdfminer`, node pdf libraries → read_pdf (zero-dependency built-in extractor).
|
|
4
|
+
|
|
5
|
+
Parameters:
|
|
6
|
+
- path (required): PDF file path (relative to cwd or absolute).
|
|
7
|
+
- pages: which pages to extract, e.g. `"1-3,5"` (1-based PDF page numbers, ranges inclusive). Default: all pages; at most 50 pages per call (larger documents need a `pages` selection).
|
|
8
|
+
|
|
9
|
+
What it handles:
|
|
10
|
+
- Text PDFs from real producers: Chrome/Edge print, Word, LibreOffice, LaTeX, Quartz — xref tables and xref streams, object streams, Flate + PNG predictors, Type0/CID fonts with ToUnicode CMaps (bfchar/bfrange), simple fonts via WinAnsi/StandardEncoding/MacRoman (+ /Differences), TJ kerning, ligature ActualText spans, and double/triple-column layout (light x-cluster column detection; tables are NOT reconstructed — table cell text reads in row/column flow).
|
|
11
|
+
- Scanned/image-only pages (no text operators): the page image is returned via the multimodal channel (`{ text, images }` JSON, like read_image) so a vision-capable model can read it. DCTDecode (JPEG) images pass through; 8-bit Flate gray/RGB images are converted to PNG. This needs a multimodal (vision) provider — under a text-only model such pages degrade to a hint telling you to switch providers.
|
|
12
|
+
|
|
13
|
+
Limits & refusals (explicit, never silent):
|
|
14
|
+
- Text-only results follow the read-family pipeline: extraction over 64KB is saved to the standard offload file (preview + path returned — page the file with read). When scanned-page images are attached, the JSON envelope rides the multimodal channel inline; text is capped at 60KB and images at 25MB cumulative per call (per-image 15MB) — narrow the pages range to read large scans in batches.
|
|
15
|
+
- Encrypted PDFs (/Encrypt) are refused with a clear message — decrypt first.
|
|
16
|
+
- Unsupported forms (Type3 glyph runs without ToUnicode, symbolic fonts without mappings, LZW-compressed streams, CMap-preset fonts without ToUnicode, JBIG2/JPX/CCITT images, palette/CMYK images) produce warnings or inline notes — output is best-effort and never silently wrong.
|
|
17
|
+
- Files >100MB refused; results never inflate past 512MB per stream (hostile-PDF guards: xref chains, recursion, operand floods).
|
|
18
|
+
|
|
19
|
+
Notes:
|
|
20
|
+
- Extraction is text-layer only — it does not OCR. Scanned pages without a text layer rely on the multimodal image channel.
|
|
21
|
+
- Layout is best-effort for irregular designs (rotated text, complex tables, text boxes).
|
package/src/tools/repomap.mjs
CHANGED
|
@@ -297,7 +297,7 @@ export function repoOutlineTool(db, cwd) {
|
|
|
297
297
|
return {
|
|
298
298
|
name: "repo_outline",
|
|
299
299
|
description:
|
|
300
|
-
"Show the project's file dependency outline: which files import/export from which, and what symbols they export. Use when you need to understand the project structure, find where a function is defined, or see what files depend on a module. Pass a path to focus on a single file's relationships.",
|
|
300
|
+
"Show the project's file dependency outline: which files import/export from which, and what symbols they export. Use when you need to understand the project structure, find where a function is defined, or see what files depend on a module. Pass a path to focus on a single file's relationships. Find code by keyword or snippet with code_search.",
|
|
301
301
|
parameters: {
|
|
302
302
|
type: "object",
|
|
303
303
|
properties: {
|
package/src/tools/shared.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { spawn, execFileSync, execFile } from "node:child_process"
|
|
7
7
|
import { readFileSync, existsSync, realpathSync, readdirSync, statSync, openSync, readSync, closeSync } from "node:fs"
|
|
8
|
-
import { dirname, join, resolve
|
|
8
|
+
import { dirname, join, resolve } from "node:path"
|
|
9
9
|
import { fileURLToPath } from "node:url"
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
@@ -170,7 +170,7 @@ export function toOpenAISchema(tool) {
|
|
|
170
170
|
/** Strip ANSI escape sequences */
|
|
171
171
|
export function sanitizeOutput(s) {
|
|
172
172
|
return s
|
|
173
|
-
|
|
173
|
+
// 有意为之:控制字符协议/转义序列剥离正则(ANSI/⟦ev⟧/SGR/history 双线分隔)
|
|
174
174
|
.replace(/\x1b\[[0-9;?]*[\x40-\x7E]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][0-9A-B]|\x1b[=>#][0-9]?/g, "")
|
|
175
175
|
.replace(/\r\n/g, "\n")
|
|
176
176
|
.replace(/\r/g, "\n")
|
|
@@ -276,27 +276,14 @@ function realCwd(cwd) {
|
|
|
276
276
|
return realCwdCache.get(cwd)
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
/**
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
// matches both / and \ traversal on the respective platform.
|
|
283
|
-
const rel = relative(cwd, resolved)
|
|
284
|
-
if (isAbsolute(rel) || rel === ".." || rel.startsWith(".." + sep)) {
|
|
285
|
-
throw new Error(`Access denied outside working directory: ${p}`)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Resolve a user-supplied path relative to cwd, asserting it stays within cwd */
|
|
279
|
+
/** Resolve a user-supplied path relative to cwd — no directory boundary assertion
|
|
280
|
+
* (§10.1 2026-09-02: workspace confinement removed; resolveInCwd ≡ resolveExternal,
|
|
281
|
+
* trust model + approval gate is the only guard — same boundary as bash). */
|
|
290
282
|
export function resolveInCwd(ctx, p) {
|
|
291
|
-
|
|
292
|
-
const resolved = resolve(cwd, p)
|
|
293
|
-
assertInside(cwd, resolved, p)
|
|
294
|
-
const real = realpathNearest(resolved)
|
|
295
|
-
assertInside(cwd, real, p)
|
|
296
|
-
return resolved
|
|
283
|
+
return resolveExternal(ctx, p)
|
|
297
284
|
}
|
|
298
285
|
|
|
299
|
-
/** Resolve a path relative to cwd without boundary check —
|
|
286
|
+
/** Resolve a path relative to cwd without boundary check — kept for compatibility (≡ resolveInCwd) */
|
|
300
287
|
export function resolveExternal(ctx, p) {
|
|
301
288
|
const cwd = realCwd(ctx.cwd)
|
|
302
289
|
return resolve(cwd, p)
|
|
@@ -309,8 +296,8 @@ export function shellSegments(command) {
|
|
|
309
296
|
|
|
310
297
|
/**
|
|
311
298
|
* Blank out quoted regions (single/double/backtick) with spaces, preserving length.
|
|
312
|
-
* Lets
|
|
313
|
-
* e.g. `node -e "if (a > b) …"` comparisons are not
|
|
299
|
+
* Lets detectDanger ignore shell metacharacters inside quoted script bodies —
|
|
300
|
+
* e.g. `node -e "if (a > b) …"` comparisons are not danger signals.
|
|
314
301
|
*/
|
|
315
302
|
function blankQuoted(command) {
|
|
316
303
|
let out = ""
|
|
@@ -321,8 +308,8 @@ function blankQuoted(command) {
|
|
|
321
308
|
if (ch === "\\" && quote !== "'") { out += " "; i++; out += " "; continue }
|
|
322
309
|
if (ch === quote) { quote = null; out += " "; continue }
|
|
323
310
|
// Backticks are COMMAND SUBSTITUTION — the content executes, so it must
|
|
324
|
-
// stay visible to
|
|
325
|
-
//
|
|
311
|
+
// stay visible to detectDanger (echo `cat > /tmp/x` still runs). Only '
|
|
312
|
+
// and " are literal regions.
|
|
326
313
|
out += quote === "`" ? ch : " "
|
|
327
314
|
} else if (ch === "'" || ch === '"' || ch === "`") {
|
|
328
315
|
quote = ch
|
|
@@ -334,14 +321,6 @@ function blankQuoted(command) {
|
|
|
334
321
|
return out
|
|
335
322
|
}
|
|
336
323
|
|
|
337
|
-
/** Detect shell output/input redirection (> >> < followed by filename) outside quoted regions.
|
|
338
|
-
* Backtick contents count (command substitution executes); fd-prefixed forms
|
|
339
|
-
* (2> file, 1>> file) count too. */
|
|
340
|
-
export function hasFileRedirection(command) {
|
|
341
|
-
const bare = blankQuoted(command)
|
|
342
|
-
return /(^|[\s;&|0-9])>{1,2}\s*\S/.test(bare) || /(^|[\s;&|0-9])<\s*\S/.test(bare)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
324
|
/**
|
|
346
325
|
* Whether a single command segment is destructive — ALWAYS FALSE (deliberate).
|
|
347
326
|
*
|
package/src/tools/system.mjs
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
BASH_TIMEOUT_MS,
|
|
7
7
|
IGNORED_DIRS,
|
|
8
8
|
resolveInCwd,
|
|
9
|
-
hasFileRedirection,
|
|
10
9
|
globToRegex,
|
|
11
10
|
normalizeEOL,
|
|
12
11
|
} from "./shared.mjs";
|
|
@@ -51,20 +50,6 @@ function posixSyntaxHint(command) {
|
|
|
51
50
|
// bash — command execution with safety gates
|
|
52
51
|
// ====================================================================
|
|
53
52
|
|
|
54
|
-
/**
|
|
55
|
-
* Pre-execution safety checks for bash commands.
|
|
56
|
-
* Layers: file redirection (guides toward structured tools, not a security gate).
|
|
57
|
-
* Destructive commands (rm -rf, DROP TABLE, ...) are deliberately NOT rejected:
|
|
58
|
-
* a determined model bypasses text matching anyway — real security is at the
|
|
59
|
-
* tool approval layer plus snapshot backups (gitGuardSnapshot / checkpoint).
|
|
60
|
-
* Git destructive ops: snapshot-then-proceed, never block.
|
|
61
|
-
*/
|
|
62
|
-
function checkBashSafety(command, cwd) {
|
|
63
|
-
if (hasFileRedirection(command)) {
|
|
64
|
-
throw new Error("File redirection via bash is not allowed — use the write/edit/insert_after tools instead")
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
53
|
/**
|
|
69
54
|
* Build environment for child process.
|
|
70
55
|
* Passes through all parent env vars, with non-interactive overrides (EDITOR/PAGER/TERM).
|
|
@@ -113,11 +98,12 @@ function killProcessTree(child) {
|
|
|
113
98
|
* cannot help (it was taken before the code was written); only a snapshot taken
|
|
114
99
|
* immediately before the destructive command can.
|
|
115
100
|
*
|
|
116
|
-
* This is
|
|
117
|
-
* runs
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
101
|
+
* This is the defense-in-depth guard: a WIDE match that snapshots before the command
|
|
102
|
+
* runs — the command itself is NEVER blocked (a determined model bypasses text
|
|
103
|
+
* matching anyway; the real gate is the approval layer). A snapshot alone is not
|
|
104
|
+
* enough: the model may retry a variant that slips through the exact matcher
|
|
105
|
+
* (e.g. `git checkout HEAD -- .`), or run git outside the bash tool. The snapshot
|
|
106
|
+
* taken here survives all of those paths.
|
|
121
107
|
*
|
|
122
108
|
* Matching is intentionally WIDE (false positives are harmless — one extra snapshot;
|
|
123
109
|
* a missed match is a data-loss disaster).
|
|
@@ -266,7 +252,6 @@ export const bashTool = {
|
|
|
266
252
|
// guard anyway. Instead: snapshot every uncommitted file first, then ALLOW
|
|
267
253
|
// the command. The snapshot makes the rollback reversible (defense in depth:
|
|
268
254
|
// the wide matcher also covers variants like `git checkout HEAD -- .`).
|
|
269
|
-
checkBashSafety(args.command, ctx.cwd)
|
|
270
255
|
const guard = await gitGuardSnapshot(args.command, ctx.cwd)
|
|
271
256
|
const result = await runBash(args.command, ctx.cwd, {
|
|
272
257
|
timeout: args.timeout ?? BASH_TIMEOUT_MS,
|
package/src/tools/tree.md
CHANGED
|
@@ -10,4 +10,5 @@ Parameters:
|
|
|
10
10
|
Notes:
|
|
11
11
|
- Capped at 200 entries.
|
|
12
12
|
- Directories end with `/`; tree-drawing uses `├──`/`└──`/`│`.
|
|
13
|
-
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
13
|
+
- Use depth for a shallow overview; use `ls` for one directory, `glob` for a specific file pattern.
|
|
14
|
+
- Returns the directory text tree — directories first (`dir/`), files after, both sorted, capped at 200 entries.
|
package/src/tools/web.mjs
CHANGED
|
@@ -4,6 +4,8 @@ import { proxyFetch } from "../proxy.mjs";
|
|
|
4
4
|
|
|
5
5
|
export const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
6
6
|
const FETCH_TIMEOUT = 15_000
|
|
7
|
+
// §14 D-TF3:网络失败错误文本追加 proxy 提示——纯文本提示,不自动路由(2026-08-31 裁定:proxy 显式传不自动应用)
|
|
8
|
+
const PROXY_HINT = "network failure — retry with proxy: 'http://host:port' if the target is blocked"
|
|
7
9
|
|
|
8
10
|
// ── Web search (Bing; direct by default, through proxy when configured and web toggle on) ──
|
|
9
11
|
|
|
@@ -204,19 +206,19 @@ export const fetchTool = {
|
|
|
204
206
|
const r = resolveRedirectTarget(loc, args.url)
|
|
205
207
|
if (r.error) throw new Error(`fetch failed: ${r.error}`)
|
|
206
208
|
const r2 = await proxyFetch(r.target, { headers: { "User-Agent": UA } }, proxyUri)
|
|
207
|
-
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}`)
|
|
209
|
+
if (!r2.ok) throw new Error(`fetch failed: HTTP ${r2.status}\n${PROXY_HINT}`)
|
|
208
210
|
const ct2 = headerOf(r2, "content-type") ?? ""
|
|
209
211
|
const b2 = await r2.text()
|
|
210
212
|
if (ct2.includes("text/html")) { const t = htmlToText(b2); return truncate(t + detectSparseHtml(b2, t)) }
|
|
211
213
|
return truncate(b2)
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
throw new Error(`fetch failed: HTTP ${response.status}`)
|
|
216
|
+
throw new Error(`fetch failed: HTTP ${response.status}\n${PROXY_HINT}`)
|
|
215
217
|
}
|
|
216
218
|
const ct = headerOf(response, "content-type") ?? ""
|
|
217
219
|
const body = await response.text()
|
|
218
220
|
if (ct.includes("text/html")) { const t = htmlToText(body); return truncate(t + detectSparseHtml(body, t)) }
|
|
219
221
|
return truncate(body)
|
|
220
|
-
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}`, { cause: e }) }
|
|
222
|
+
} catch (e) { throw new Error(`fetch failed: ${e.cause?.code ?? e.message}\n${PROXY_HINT}`, { cause: e }) }
|
|
221
223
|
},
|
|
222
224
|
}
|
package/src/tools/websearch.md
CHANGED
|
@@ -8,7 +8,8 @@ Parameters:
|
|
|
8
8
|
- proxy: http://host:port explicit proxy (optional) — use ONLY when passed; no proxy = direct. config.json proxy is NOT auto-applied (2026-08-31 ruling); Bing/foreign sites usually need a proxy, domestic targets don't
|
|
9
9
|
|
|
10
10
|
Notes:
|
|
11
|
-
- Before searching the web, call
|
|
11
|
+
- Before searching the web, call the memory tool (action: search) first — you may already know the answer from a previous session. Only reach for websearch if memory comes up empty.
|
|
12
|
+
- Runs synchronously — returns when the search completes (all selected engines run in parallel).
|
|
12
13
|
- Use this for information that is NOT in the local codebase — current docs, error messages, API references
|
|
13
14
|
- Follow up with `fetch` to read full pages from the results
|
|
14
15
|
- **Weak engine warning**: Bing's index is noisy for technical queries — if a first websearch returns irrelevant/townhall-grade results, DO NOT retry the same query. Configure a search MCP tool (e.g. `glm-websearch` via the MCP config) for technical lookups; websearch is the fallback.
|
package/src/tools/write.md
CHANGED
|
@@ -5,5 +5,7 @@ Parameters:
|
|
|
5
5
|
- content (required): Full content to write
|
|
6
6
|
|
|
7
7
|
Notes:
|
|
8
|
+
- write replaces the WHOLE file — read it first and confirm you intend to rewrite it entirely; for a small change use edit / insert_after.
|
|
9
|
+
- Returns `Wrote N chars to <path>` + git diff + syntax-check note.
|
|
8
10
|
- This overwrites the entire file — use `edit` for targeted changes
|
|
9
11
|
- The file is atomic: it either writes completely or fails
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* trace-store.mjs — §18.6 完整轨迹存档(AGENT-LOOP.md §18.6 D-TR1..TR8 权威规格)。
|
|
3
|
+
*
|
|
4
|
+
* 完整请求-响应轨迹落盘:每次 chat() 调用一个 JSONL 文件到
|
|
5
|
+
* ~/.thincoder/traces/YYYY-MM-DD/<sessionKey>-<seq>.jsonl。采集点唯一 = core.mjs
|
|
6
|
+
* chat() 导出出口(所有模型调用——主回合/消化轮/compress/distill/advisor/子代理/
|
|
7
|
+
* auto-think/consult——都经该函数)——续写/重试在出口已合并(reasoning 全量)。
|
|
8
|
+
*
|
|
9
|
+
* 纪律(与 log.mjs 同源惯例):
|
|
10
|
+
* - 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 同步预留(原子号位),
|
|
11
|
+
* 写盘异步(node:fs/promises——不 await);落盘失败静默降级(不抛错、不阻塞
|
|
12
|
+
* chat() 返回)。recordChatTrace 返回落盘 promise——仅供测试/显式消费方 await。
|
|
13
|
+
* - 测试隔离:node --test 进程(NODE_TEST_CONTEXT)默认不写盘——防测试事件污染真实
|
|
14
|
+
* 轨迹目录(既有测试跑真实 agent 管线会产生数百次 chat 调用);显式设置
|
|
15
|
+
* THINCODER_TRACES_DIR 强制写入该目录(traces.test.mjs 用它隔离临时目录——
|
|
16
|
+
* 与 log.mjs 的 THINCODER_LOG_DIR 同惯例)。
|
|
17
|
+
* - 脱敏(D-TR2):复用 log.mjs 字段名黑名单(apikey/designtoken/password/secret/
|
|
18
|
+
* token/authorization/proxyuri/proxy)+ SECRET_FORM 形态扫描(redactSecret)——对
|
|
19
|
+
* messages/content/reasoning/toolCalls/error 全字段递归应用;不发明新遮蔽模式。
|
|
20
|
+
* - 容量有界(N-TR1):单条轨迹一个文件,大小不限("成本不是问题"——不截断、不
|
|
21
|
+
* 脱漏行);目录按日组织(YYYY-MM-DD),可按日/会话过滤;不自动清理(D-TR8——
|
|
22
|
+
* 清理机制记 docs/TODO.md 技术组)。
|
|
23
|
+
* - seq = 当日目录内最大已有 seq + 1(D-TR3——跨会话/进程重启不覆写既有旧轨迹——
|
|
24
|
+
* 18.6.1 评审 #3)。
|
|
25
|
+
* - 日期分日按本地时区(D-TR3——2026-09-04 fix round1:初版 toISOString()=UTC——
|
|
26
|
+
* 本地 00:00-08:00 的调用会落进前一日目录——改本地日期字符串 YYYY-MM-DD——T-TR12)。
|
|
27
|
+
* - isContinuation(D-TR1——2026-09-04 fix round1):续写/重试链标记——true = 该调用
|
|
28
|
+
* 是续写链的一环(core.mjs 续写递归传出 logCtx.isContinuation);新调用 false。
|
|
29
|
+
* 记录不输出 round 字段(全设计无 round 定义——删——不发明无来源字段)。
|
|
30
|
+
*/
|
|
31
|
+
import { readdirSync, existsSync } from "node:fs"
|
|
32
|
+
import { appendFile, mkdir, readdir, stat, unlink, rmdir } from "node:fs/promises"
|
|
33
|
+
import { join } from "node:path"
|
|
34
|
+
import { createHash } from "node:crypto"
|
|
35
|
+
import { configDir } from "../config.mjs"
|
|
36
|
+
import { redactSecret, errText, classifyErr } from "../log.mjs"
|
|
37
|
+
import { normalizeCwd } from "../session-slots.mjs"
|
|
38
|
+
|
|
39
|
+
/** 轨迹根目录:THINCODER_TRACES_DIR(测试隔离/override——同 THINCODER_LOG_DIR
|
|
40
|
+
* 惯例)> ~/.thincoder/traces(configDir——D-TR3——与 sessions/ 同域)。 */
|
|
41
|
+
export function tracesRoot() {
|
|
42
|
+
return process.env.THINCODER_TRACES_DIR ?? join(configDir, "traces")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 写门(测试隔离):test runner 进程(NODE_TEST_CONTEXT)默认跳过——除显式
|
|
46
|
+
* THINCODER_TRACES_DIR override(traces.test.mjs 隔离临时目录)。 */
|
|
47
|
+
function writeEnabled() {
|
|
48
|
+
if (process.env.NODE_TEST_CONTEXT && !process.env.THINCODER_TRACES_DIR) return false
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** sessionKey = sha1(normalizeCwd(cwd))[:12](D-TR3——与 session-slots sessionPath
|
|
53
|
+
* 同算法同 cwd 归一——两端 hash 一致)。 */
|
|
54
|
+
export function traceSessionKey(cwd) {
|
|
55
|
+
return createHash("sha1").update(normalizeCwd(cwd)).digest("hex").slice(0, 12)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 本地时区日期字符串 YYYY-MM-DD(D-TR3——2026-09-04 fix round1:初版
|
|
59
|
+
* toISOString()=UTC 日期——本地 00:00-08:00 落前一日目录;本地日期才是用户视角的
|
|
60
|
+
* "今天"——T-TR12)。recordChatTrace 与测试读取 helpers 共用同一实现——不双写。 */
|
|
61
|
+
export function localDateStr(date = new Date()) {
|
|
62
|
+
const y = date.getFullYear()
|
|
63
|
+
const m = String(date.getMonth() + 1).padStart(2, "0")
|
|
64
|
+
const d = String(date.getDate()).padStart(2, "0")
|
|
65
|
+
return `${y}-${m}-${d}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Test hooks(_rateHooks 同惯例——rate.mjs:测试可替换 now——T-TR12 注入本地日 ≠
|
|
69
|
+
* UTC 日的时刻验证分日;生产永远走真实时钟)。 */
|
|
70
|
+
export const _traceHooks = {
|
|
71
|
+
now: () => new Date(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 当日轨迹目录(D-TR3:traces/YYYY-MM-DD——分日组织——N-TR1)。 */
|
|
75
|
+
export function tracesDirFor(dateStr) {
|
|
76
|
+
return join(tracesRoot(), dateStr)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 进程内 seq 预留表:同步预留(原子——异步写盘在途时并发调用不撞号);键 = 目录。
|
|
80
|
+
// 跨进程/重启由磁盘 max 兜底(同键无预留时磁盘值即事实——T-TR10)。
|
|
81
|
+
const _reservedSeq = new Map()
|
|
82
|
+
|
|
83
|
+
/** seq = max(当日目录最大已有 seq, 进程内已预留) + 1(D-TR3——跨会话/进程重启
|
|
84
|
+
* 不覆写——T-TR8/T-TR10;同步预留 = 异步写盘启动前的原子号位分配)。 */
|
|
85
|
+
export function nextTraceSeq(dateStr) {
|
|
86
|
+
const dir = tracesDirFor(dateStr)
|
|
87
|
+
let max = 0
|
|
88
|
+
if (existsSync(dir)) {
|
|
89
|
+
let names
|
|
90
|
+
try {
|
|
91
|
+
names = readdirSync(dir)
|
|
92
|
+
} catch {
|
|
93
|
+
// 目录不可读——按预留表续号(不静默撞号)
|
|
94
|
+
max = _reservedSeq.get(dir) ?? 0
|
|
95
|
+
_reservedSeq.set(dir, max + 1)
|
|
96
|
+
return max + 1
|
|
97
|
+
}
|
|
98
|
+
for (const name of names) {
|
|
99
|
+
const m = name.match(/-(\d+)\.jsonl$/)
|
|
100
|
+
if (m) max = Math.max(max, Number(m[1]))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const seq = Math.max(max, _reservedSeq.get(dir) ?? 0) + 1
|
|
104
|
+
_reservedSeq.set(dir, seq)
|
|
105
|
+
return seq
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 开关(D-TR6):traces.enabled 缺省 on(默认全采集);logCtx.traces === false
|
|
109
|
+
* 显式关闭(调用点读取 agent.config.traces.enabled——D-TR6)。 */
|
|
110
|
+
export function tracesEnabled(logCtx) {
|
|
111
|
+
return logCtx?.traces !== false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** 递归脱敏(D-TR2):对 messages/content/reasoning/toolCalls/error 全字段应用——
|
|
115
|
+
* 字段名命中黑名单 → 遮蔽;字符串内容命中密钥形态(sk-/Bearer/-key=)→ 截断到
|
|
116
|
+
* 形态前 + 标记。数组/对象递归(消息 content 可为 parts 数组、toolCalls 嵌套)。 */
|
|
117
|
+
function redactValue(fieldKey, value) {
|
|
118
|
+
if (typeof value === "string") return redactSecret(fieldKey, value)
|
|
119
|
+
if (Array.isArray(value)) return value.map((v) => redactValue(fieldKey, v))
|
|
120
|
+
if (value && typeof value === "object") {
|
|
121
|
+
const out = {}
|
|
122
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactValue(k, v)
|
|
123
|
+
return out
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 收集一次 chat 调用轨迹(D-TR1 字段集)——fire-and-forget(F-TR3)。
|
|
130
|
+
* 由 core.mjs chat() 导出出口调用(唯一采集点——N-TR2);签名零参数膨胀——
|
|
131
|
+
* 数据全部来自调用方已传入的 opts(logCtx 元数据——D-TR4)+ provider + result/error。
|
|
132
|
+
*
|
|
133
|
+
* @param {Object} provider chat() 的 provider 参数(provider/model 字段来源)
|
|
134
|
+
* @param {Object} opts chat() 原始 opts —— messages(输入)与 logCtx(元数据:
|
|
135
|
+
* role/depth/kind/session/cwd/stage/turn/traces——调用点增补,D-TR4)
|
|
136
|
+
* @param {Object|null} result chatImpl 返回值(成功路径——content/reasoning/
|
|
137
|
+
* toolCalls/usage/finishReason;N-TR3:出口汇总——续写/重试后全量)
|
|
138
|
+
* @param {Error|null} error chat() 抛出的错误(失败路径——D-TR5:error(errText
|
|
139
|
+
* 截断 + 类别)+ finishReason:null——失败轨迹恰是分析纠结点最有效的材料)
|
|
140
|
+
*/
|
|
141
|
+
export function recordChatTrace(provider, opts = {}, result = null, error = null) {
|
|
142
|
+
if (!writeEnabled()) return
|
|
143
|
+
const logCtx = opts.logCtx ?? {}
|
|
144
|
+
if (!tracesEnabled(logCtx)) return
|
|
145
|
+
// cwd/session 经 logCtx 增补(调用点传 agent.cwd / agent._sessionStart——
|
|
146
|
+
// D-TR3 的 cwdHash/命名与 D-TR1 的 session 字段所需);无 agent 作用域的调用点
|
|
147
|
+
// 回退 process.cwd()——CLI 会话即工作区。该类点开关状态:distill 已含
|
|
148
|
+
// traces 开关(D-TR6 fix round1);auto-think 已闭环(D-TS12——chat 调用点
|
|
149
|
+
// logCtx 全字段补传:traces/session/cwd/role/depth/kind——无残留点)。
|
|
150
|
+
const cwd = logCtx.cwd ?? process.cwd()
|
|
151
|
+
// D-TR3(fix round1):分日按本地时区(_traceHooks.now——T-TR12 注入点);
|
|
152
|
+
// ts 与 dateStr 同源(同一时刻)——记录时间戳与目录日不撕裂。
|
|
153
|
+
const now = _traceHooks.now()
|
|
154
|
+
const dateStr = localDateStr(now)
|
|
155
|
+
const seq = nextTraceSeq(dateStr)
|
|
156
|
+
const sessionKey = traceSessionKey(cwd)
|
|
157
|
+
const record = {
|
|
158
|
+
ts: now.toISOString(),
|
|
159
|
+
session: logCtx.session ?? null,
|
|
160
|
+
cwdHash: createHash("sha1").update(normalizeCwd(cwd)).digest("hex"),
|
|
161
|
+
role: logCtx.role ?? null,
|
|
162
|
+
depth: logCtx.depth ?? null,
|
|
163
|
+
turn: logCtx.turn ?? null,
|
|
164
|
+
provider: provider?.name ?? provider?.model ?? "unknown",
|
|
165
|
+
model: provider?.model ?? "",
|
|
166
|
+
stage: logCtx.stage ?? null,
|
|
167
|
+
kind: logCtx.kind ?? null,
|
|
168
|
+
// D-TR1(fix round1):续写/重试链标记——true = 该调用是续写链的一环
|
|
169
|
+
// (core.mjs 续写递归传出);新调用缺省 false——round 字段已删(无来源)。
|
|
170
|
+
isContinuation: logCtx.isContinuation === true,
|
|
171
|
+
messages: redactValue("messages", opts.messages ?? []),
|
|
172
|
+
content: redactValue("content", result?.content ?? null),
|
|
173
|
+
reasoning: redactValue("reasoning", result?.reasoning ?? null),
|
|
174
|
+
toolCalls: redactValue("toolCalls", result?.toolCalls ?? null),
|
|
175
|
+
usage: result?.usage ?? null,
|
|
176
|
+
finishReason: result?.finishReason ?? null,
|
|
177
|
+
}
|
|
178
|
+
if (error != null) {
|
|
179
|
+
// D-TR5:错误路径轨迹——error(errText 截断 + 类别)+ finishReason:null
|
|
180
|
+
record.error = {
|
|
181
|
+
err: redactValue("err", errText(error, 500)),
|
|
182
|
+
kind: classifyErr(error, opts.signal),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// 真 fire-and-forget(F-TR3——模型调用路径零额外阻塞):seq 已在上面同步预留
|
|
186
|
+
// (原子号位——写盘在途并发不撞号);写盘异步(不 await——chat() 出口立即返回)。
|
|
187
|
+
// 返回值 = 落盘 promise(测试 await 用;chat() 不消费——fire-and-forget 语义)。
|
|
188
|
+
return (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const dir = tracesDirFor(dateStr)
|
|
191
|
+
await mkdir(dir, { recursive: true }) // D-TR3:写前建目录(与 sessions/tool-results 同惯例)
|
|
192
|
+
await appendFile(join(dir, `${sessionKey}-${seq}.jsonl`), JSON.stringify(record) + "\n", "utf8")
|
|
193
|
+
} catch {
|
|
194
|
+
// F-TR3:落盘失败静默降级——不抛错、不阻塞 chat() 返回
|
|
195
|
+
}
|
|
196
|
+
})()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* D-TR10(2026-09-05 用户裁定——发布隐私 + 磁盘卫生):启动清理——删除 traces 根下
|
|
201
|
+
* mtime 超过保留期的轨迹文件(保留期 = config.traces.retentionHours,默认 24h);
|
|
202
|
+
* 删空的日期目录(YYYY-MM-DD)。目录里非 .jsonl 文件不碰。CLI 启动点 fire-and-forget
|
|
203
|
+
* 调用(不 await——不阻塞启动——失败静默——与轨迹写盘同纪律)。返回删除文件数。
|
|
204
|
+
*/
|
|
205
|
+
export async function cleanupTraces({ dir = tracesRoot(), retentionHours = 24 } = {}) {
|
|
206
|
+
const cutoff = Date.now() - retentionHours * 3_600_000
|
|
207
|
+
let days
|
|
208
|
+
try { days = await readdir(dir) } catch { return 0 } // 目录不存在/不可读 → 无事可做
|
|
209
|
+
let removed = 0
|
|
210
|
+
for (const day of days) {
|
|
211
|
+
const dayDir = join(dir, day)
|
|
212
|
+
try { if (!(await stat(dayDir)).isDirectory()) continue } catch { continue }
|
|
213
|
+
let names
|
|
214
|
+
try { names = await readdir(dayDir) } catch { continue }
|
|
215
|
+
for (const n of names) {
|
|
216
|
+
if (!n.endsWith(".jsonl")) continue
|
|
217
|
+
try {
|
|
218
|
+
if ((await stat(join(dayDir, n))).mtimeMs < cutoff) { await unlink(join(dayDir, n)); removed++ }
|
|
219
|
+
} catch { /* 单个文件失败不影响其余 */ }
|
|
220
|
+
}
|
|
221
|
+
try { if ((await readdir(dayDir)).length === 0) await rmdir(dayDir) } catch {}
|
|
222
|
+
}
|
|
223
|
+
return removed
|
|
224
|
+
}
|