@thincoder/core 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abort-provenance.mjs +116 -0
- package/advisor/citations.mjs +139 -0
- package/advisor/compaction.mjs +174 -0
- package/advisor/convergence.mjs +80 -0
- package/advisor/history.mjs +77 -0
- package/advisor/loop.mjs +288 -0
- package/advisor/messages.mjs +299 -0
- package/advisor/notice.mjs +141 -0
- package/advisor/project-context.mjs +197 -0
- package/advisor/repos.mjs +150 -0
- package/advisor/run.mjs +190 -0
- package/advisor/truncate.mjs +57 -0
- package/advisor.mjs +281 -0
- package/agent/child-marks.mjs +24 -0
- package/agent/completion.mjs +145 -0
- package/agent/dispatch.mjs +493 -0
- package/agent/family-tools.mjs +174 -0
- package/agent/helpers.mjs +412 -0
- package/agent/post-turn.mjs +70 -0
- package/agent/record-results.mjs +174 -0
- package/agent/relay-prefix.mjs +39 -0
- package/agent/run-stages.mjs +244 -0
- package/agent/setup-reminders.mjs +199 -0
- package/agent/setup.mjs +234 -0
- package/agent/spawn-child.mjs +258 -0
- package/agent/suspension.mjs +240 -0
- package/agent/write-gate.mjs +87 -0
- package/agent-tools/advisor-async.mjs +481 -0
- package/agent-tools/advisor-settle.mjs +240 -0
- package/agent-tools/advisor.mjs +280 -0
- package/agent-tools/async-discard.mjs +143 -0
- package/agent-tools/async-settle.mjs +299 -0
- package/agent-tools/batch-segment.mjs +265 -0
- package/agent-tools/child-permission.mjs +45 -0
- package/agent-tools/consult.mjs +471 -0
- package/agent-tools/design-token.mjs +117 -0
- package/agent-tools/digest-budget.mjs +76 -0
- package/agent-tools/eng.mjs +102 -0
- package/agent-tools/escalate-async.mjs +302 -0
- package/agent-tools/goal.mjs +119 -0
- package/agent-tools/panel-blocks.mjs +24 -0
- package/agent-tools/parent-channel.mjs +231 -0
- package/agent-tools/plan.mjs +86 -0
- package/agent-tools/read-history.mjs +309 -0
- package/agent-tools/recent-changes.mjs +24 -0
- package/agent-tools/review-facts.mjs +31 -0
- package/agent-tools/settings.mjs +268 -0
- package/agent-tools/skill.mjs +63 -0
- package/agent-tools/spawn-gates.mjs +109 -0
- package/agent-tools/subagent-actions.mjs +496 -0
- package/agent-tools/subagent-async.mjs +456 -0
- package/agent-tools/subagent-panel.mjs +160 -0
- package/agent-tools/subagent-run.mjs +208 -0
- package/agent-tools/subagent-scheduler.mjs +446 -0
- package/agent-tools/subagent-spawn.mjs +478 -0
- package/agent-tools/subagent.mjs +419 -0
- package/agent-tools/task.mjs +87 -0
- package/agent-tools/timer.mjs +46 -0
- package/agent-tools/verify.mjs +295 -0
- package/agent-tools.mjs +23 -0
- package/agent.mjs +430 -0
- package/auto-think.mjs +115 -0
- package/compress-form.mjs +24 -0
- package/config-io.mjs +277 -0
- package/config-migrate.mjs +178 -0
- package/config-presets.mjs +49 -0
- package/config.mjs +419 -0
- package/context.mjs +495 -0
- package/conventions.mjs +223 -0
- package/embedding.mjs +120 -0
- package/escape.mjs +152 -0
- package/expand-home.mjs +16 -0
- package/explore-distill.mjs +152 -0
- package/generate-title.mjs +123 -0
- package/git/checkpoint.mjs +448 -0
- package/git/gitmem.mjs +100 -0
- package/history-window.mjs +179 -0
- package/hooks.mjs +108 -0
- package/i18n.mjs +106 -0
- package/index-bin.mjs +48 -0
- package/index-discover.mjs +176 -0
- package/ledger-cmd.mjs +209 -0
- package/ledger-db.mjs +86 -0
- package/ledger-surface.mjs +76 -0
- package/ledger.mjs +202 -0
- package/log.mjs +195 -0
- package/manifest.mjs +338 -0
- package/markdown.mjs +106 -0
- package/mcp/helpers.mjs +51 -0
- package/mcp/transport-http.mjs +248 -0
- package/mcp/transport-stdio.mjs +140 -0
- package/mcp/transport-ws.mjs +122 -0
- package/mcp.mjs +295 -0
- package/memory/code-index.mjs +219 -0
- package/memory/code-sync.mjs +427 -0
- package/memory/core.mjs +318 -0
- package/memory/delete.mjs +242 -0
- package/memory/docs.mjs +431 -0
- package/memory/file-walk.mjs +109 -0
- package/memory/origin.mjs +24 -0
- package/memory/scan.mjs +177 -0
- package/memory/schema.mjs +460 -0
- package/memory.mjs +21 -0
- package/model-ref.mjs +66 -0
- package/model-specs.mjs +277 -0
- package/package.json +32 -0
- package/peer-domains.mjs +265 -0
- package/peer-instances.mjs +178 -0
- package/permission.mjs +79 -0
- package/process-probe.mjs +315 -0
- package/prompt-files.mjs +113 -0
- package/prompt-overlays.mjs +78 -0
- package/prompts/advisor-design.md +43 -0
- package/prompts/advisor-round1.md +41 -0
- package/prompts/advisor-round2.md +46 -0
- package/prompts/advisor-round3.md +42 -0
- package/prompts/common.md +158 -0
- package/prompts/consult-base.md +19 -0
- package/prompts/discipline-engineering.md +123 -0
- package/prompts/discipline-normal.md +206 -0
- package/prompts/persona-coder.md +21 -0
- package/prompts/persona-eng-coder.md +41 -0
- package/prompts/persona-eng-designer.md +80 -0
- package/prompts/persona-engineering.md +160 -0
- package/prompts/persona-explore.md +15 -0
- package/prompts/persona-normal.md +35 -0
- package/prompts/persona-plan.md +27 -0
- package/provider/anthropic.mjs +225 -0
- package/provider/core.mjs +491 -0
- package/provider/errors.mjs +101 -0
- package/provider/google.mjs +257 -0
- package/provider/index.mjs +7 -0
- package/provider/list-models.mjs +163 -0
- package/provider/normalize.mjs +81 -0
- package/provider/rate.mjs +168 -0
- package/provider/responses.mjs +495 -0
- package/provider/retry.mjs +88 -0
- package/provider/sse.mjs +264 -0
- package/provider/wait-status.mjs +59 -0
- package/proxy.mjs +274 -0
- package/rules.mjs +53 -0
- package/session-gc.mjs +248 -0
- package/session-guard.mjs +59 -0
- package/session-lifecycle.mjs +305 -0
- package/session-migrate.mjs +48 -0
- package/session-rename.mjs +38 -0
- package/session-segments.mjs +99 -0
- package/session-slot-write.mjs +168 -0
- package/session-slots-manifest.mjs +264 -0
- package/session-slots.mjs +298 -0
- package/session-store.mjs +441 -0
- package/session.mjs +244 -0
- package/skills.mjs +234 -0
- package/text-budget.mjs +79 -0
- package/token-ttl.mjs +285 -0
- package/tool-docs/apply_patch.md +15 -0
- package/tool-docs/bash.md +38 -0
- package/tool-docs/delete.md +13 -0
- package/tool-docs/edit.md +30 -0
- package/tool-docs/execute.md +21 -0
- package/tool-docs/fetch.md +12 -0
- package/tool-docs/file_ops.md +17 -0
- package/tool-docs/get_current_time.md +8 -0
- package/tool-docs/git.md +54 -0
- package/tool-docs/glob.md +11 -0
- package/tool-docs/grep.md +19 -0
- package/tool-docs/hashline_edit.md +14 -0
- package/tool-docs/insert_after.md +15 -0
- package/tool-docs/lint.md +10 -0
- package/tool-docs/ls.md +12 -0
- package/tool-docs/lsp.md +10 -0
- package/tool-docs/process.md +10 -0
- package/tool-docs/question.md +16 -0
- package/tool-docs/read.md +20 -0
- package/tool-docs/read_image.md +8 -0
- package/tool-docs/tree.md +14 -0
- package/tool-docs/wait_for.md +22 -0
- package/tool-docs/websearch.md +16 -0
- package/tool-docs/write.md +11 -0
- package/tools/bash.mjs +276 -0
- package/tools/edit-batch.mjs +204 -0
- package/tools/edit-diff.mjs +388 -0
- package/tools/exec-run.mjs +43 -0
- package/tools/execute.mjs +243 -0
- package/tools/file.mjs +464 -0
- package/tools/git-checkpoint.mjs +143 -0
- package/tools/git-ext.mjs +173 -0
- package/tools/git.mjs +415 -0
- package/tools/glob-dialect.mjs +130 -0
- package/tools/index.mjs +76 -0
- package/tools/linter.mjs +120 -0
- package/tools/lsp.mjs +335 -0
- package/tools/ops.mjs +293 -0
- package/tools/patch.mjs +290 -0
- package/tools/question.mjs +26 -0
- package/tools/repomap.mjs +314 -0
- package/tools/search.mjs +248 -0
- package/tools/shared.mjs +467 -0
- package/tools/tree.mjs +81 -0
- package/tools/web.mjs +224 -0
- package/tools/write-path.mjs +191 -0
- package/traces/trace-store.mjs +303 -0
- package/undo-stack.mjs +47 -0
package/conventions.mjs
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conventions.mjs — the single authority for code / doc / temp path classification,
|
|
3
|
+
* plus the project convention declaration surface (`.thincoder/conventions.json`).
|
|
4
|
+
*
|
|
5
|
+
* Why one module: engineering-mode gates and guards (design gate, review-doc gate,
|
|
6
|
+
* mutation accounting, verify fast path) each carried their own copy of the
|
|
7
|
+
* "what counts as product code" predicate — anchored `^src/` regexes, `docs/`
|
|
8
|
+
* prefix checks, component regexes. Each copy drifted, and each hardcoded THIS
|
|
9
|
+
* repository's layout: a project whose code lives outside `src/` slipped through
|
|
10
|
+
* the design gate silently (PORTABILITY FR12 / PO-10). One classifier + one
|
|
11
|
+
* declaration file = one truth.
|
|
12
|
+
*
|
|
13
|
+
* Defaults are DATA (`DEFAULT_CODE_PATHS`) — overridable per project through the
|
|
14
|
+
* declaration file (§4.1 schema). Missing file → pure defaults (no noise);
|
|
15
|
+
* corrupt/unreadable file → defaults + console.warn + a log event (never crash,
|
|
16
|
+
* never swallow — PORTABILITY FR10).
|
|
17
|
+
*
|
|
18
|
+
* Classification vocabulary (PORTABILITY design §3.1):
|
|
19
|
+
* code — inside a declared code segment (default: the path segment `src`), or
|
|
20
|
+
* not a documentation extension; doc — documentation extension outside
|
|
21
|
+
* any code segment; temp — tmp-* name or .tmp/.temp extension.
|
|
22
|
+
*/
|
|
23
|
+
import { readFileSync } from "node:fs"
|
|
24
|
+
import { join, resolve } from "node:path"
|
|
25
|
+
import { logEvent } from "./log.mjs"
|
|
26
|
+
|
|
27
|
+
/** Default code-path segments (data, not logic — a project may replace them). */
|
|
28
|
+
export const DEFAULT_CODE_PATHS = ["src"]
|
|
29
|
+
|
|
30
|
+
/** Project declaration file, relative to the project root. */
|
|
31
|
+
export const CONVENTIONS_REL_PATH = ".thincoder/conventions.json"
|
|
32
|
+
|
|
33
|
+
/** Documentation predicate (moved here verbatim from advisor/repos.mjs — one copy). */
|
|
34
|
+
const DOC_FILE = /(?:^|[/\\])(?:LICENSE|NOTICE|CHANGELOG|AUTHORS)(?:\.\w+)?$|\.(?:md|markdown|mdx|txt|rst|adoc)$/i
|
|
35
|
+
|
|
36
|
+
/** Temp/scratch predicate (moved here verbatim from advisor/repos.mjs). */
|
|
37
|
+
const TEMP_FILE = /(?:^|[/\\])tmp-[^/\\]+$|\.(?:tmp|temp)$/i
|
|
38
|
+
|
|
39
|
+
/** True when a path is a throwaway temp file (tmp-* name or .tmp/.temp extension). */
|
|
40
|
+
export function isTempPath(p) {
|
|
41
|
+
return TEMP_FILE.test(p ?? "")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Path → segments (both separators accepted; absolute and relative alike). */
|
|
45
|
+
function segmentsOf(p) {
|
|
46
|
+
return String(p ?? "").replace(/\\/g, "/").split("/").filter(Boolean)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* True when the path contains a declared code segment sequence at any depth.
|
|
51
|
+
* Segment matching (not a prefix anchor) is what closes the nested-layout hole:
|
|
52
|
+
* `packages/foo/src/x.md` is product code, not a document. Comparison is
|
|
53
|
+
* case-insensitive — on case-insensitive filesystems `Src/x.mjs` is the same
|
|
54
|
+
* directory, and the gate must not be bypassable by casing.
|
|
55
|
+
*/
|
|
56
|
+
function hasCodeSegment(p, conv) {
|
|
57
|
+
const parts = segmentsOf(p).map((s) => s.toLowerCase())
|
|
58
|
+
const wanted = conv?.codePaths ?? DEFAULT_CODE_PATHS
|
|
59
|
+
for (const entry of wanted) {
|
|
60
|
+
const want = segmentsOf(entry).map((s) => s.toLowerCase())
|
|
61
|
+
if (want.length === 0) continue
|
|
62
|
+
for (let i = 0; i + want.length <= parts.length; i++) {
|
|
63
|
+
if (want.every((seg, j) => parts[i + j] === seg)) return true
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** "code" | "doc" | "temp" — the single classification decision.
|
|
70
|
+
* Precedence: code segment first (src/** stays product code even when the name
|
|
71
|
+
* looks scratch — the pre-existing unconditional-src rule), then temp, then a
|
|
72
|
+
* documentation extension, else code (anything not doc/temp is product code). */
|
|
73
|
+
export function classifyPath(p, conv) {
|
|
74
|
+
const s = String(p ?? "")
|
|
75
|
+
if (hasCodeSegment(s, conv)) return "code"
|
|
76
|
+
if (TEMP_FILE.test(s)) return "temp"
|
|
77
|
+
if (DOC_FILE.test(s)) return "doc"
|
|
78
|
+
return "code"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True when the path is product code (see classifyPath for the precedence). */
|
|
82
|
+
export function isCodePath(p, conv) {
|
|
83
|
+
return classifyPath(p, conv) === "code"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** True when the path is a documentation file — a doc extension that does NOT
|
|
87
|
+
* live inside a declared code segment (src/prompts/*.md is product code). */
|
|
88
|
+
export function isDocPath(p, conv) {
|
|
89
|
+
const s = String(p ?? "")
|
|
90
|
+
return DOC_FILE.test(s) && !hasCodeSegment(s, conv)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// Declaration loading (cached per project root — `clearConventionsCache()` is
|
|
95
|
+
// the test seam; declaration files change rarely and only at session scope).
|
|
96
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function normalizeExtensions(v) {
|
|
99
|
+
if (!Array.isArray(v)) return []
|
|
100
|
+
const out = []
|
|
101
|
+
for (const e of v) {
|
|
102
|
+
if (typeof e !== "string") continue
|
|
103
|
+
const t = e.trim().toLowerCase()
|
|
104
|
+
if (!t) continue
|
|
105
|
+
const ext = t.startsWith(".") ? t : `.${t}`
|
|
106
|
+
if (!out.includes(ext)) out.push(ext)
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Declared code paths replace the default (replacement, not union — §4.1). */
|
|
112
|
+
function normalizeCodePaths(v) {
|
|
113
|
+
if (!Array.isArray(v)) return null
|
|
114
|
+
const out = []
|
|
115
|
+
for (const e of v) {
|
|
116
|
+
if (typeof e !== "string") continue
|
|
117
|
+
const s = e.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "")
|
|
118
|
+
if (!s || out.includes(s)) continue
|
|
119
|
+
out.push(s)
|
|
120
|
+
}
|
|
121
|
+
return out.length > 0 ? out : null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeString(v) {
|
|
125
|
+
return typeof v === "string" && v.trim() ? v.trim() : ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Per-key type check for recognized keys (present but wrong type). §3.2/§4.1: a
|
|
129
|
+
* type error degrades WITH a warning — never silently (the fallback semantics stay
|
|
130
|
+
* per-key; only the visibility is added here). */
|
|
131
|
+
function typeErrorsOf(raw) {
|
|
132
|
+
const isObj = (v) => v !== undefined && v !== null && typeof v === "object" && !Array.isArray(v)
|
|
133
|
+
const strArray = (v) => Array.isArray(v) && v.every((x) => typeof x === "string")
|
|
134
|
+
const errs = []
|
|
135
|
+
if (raw.codePaths !== undefined && !strArray(raw.codePaths)) errs.push("codePaths must be an array of strings")
|
|
136
|
+
const idx = raw.index
|
|
137
|
+
if (idx !== undefined && !isObj(idx)) errs.push("index must be an object")
|
|
138
|
+
else if (isObj(idx)) {
|
|
139
|
+
for (const k of ["codeExtensions", "docExtensions"]) {
|
|
140
|
+
if (idx[k] !== undefined && !strArray(idx[k])) errs.push(`index.${k} must be an array of strings`)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const adv = raw.advisor
|
|
144
|
+
if (adv !== undefined && !isObj(adv)) errs.push("advisor must be an object")
|
|
145
|
+
else if (isObj(adv)) {
|
|
146
|
+
for (const k of ["docMap", "standardsDoc"]) {
|
|
147
|
+
if (adv[k] !== undefined && typeof adv[k] !== "string") errs.push(`advisor.${k} must be a string`)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return errs
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function buildConventions(raw) {
|
|
154
|
+
const codePaths = normalizeCodePaths(raw?.codePaths)
|
|
155
|
+
const codeExtensions = normalizeExtensions(raw?.index?.codeExtensions)
|
|
156
|
+
const docExtensions = normalizeExtensions(raw?.index?.docExtensions)
|
|
157
|
+
const docMap = normalizeString(raw?.advisor?.docMap)
|
|
158
|
+
const standardsDoc = normalizeString(raw?.advisor?.standardsDoc)
|
|
159
|
+
// `declared` = the declaration actually took effect (at least one recognized key
|
|
160
|
+
// honored) — the design-gate hint reads it to decide whether to point at the
|
|
161
|
+
// declaration file ("declare project conventions … to adjust").
|
|
162
|
+
const declared = Boolean(codePaths || codeExtensions.length || docExtensions.length || docMap || standardsDoc)
|
|
163
|
+
return Object.freeze({
|
|
164
|
+
declared,
|
|
165
|
+
codePaths: Object.freeze(codePaths ?? [...DEFAULT_CODE_PATHS]),
|
|
166
|
+
index: Object.freeze({
|
|
167
|
+
codeExtensions: Object.freeze(codeExtensions),
|
|
168
|
+
docExtensions: Object.freeze(docExtensions),
|
|
169
|
+
}),
|
|
170
|
+
advisor: Object.freeze({ docMap, standardsDoc }),
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Full-default conventions (no declaration) — the fallback every consumer gets. */
|
|
175
|
+
export const DEFAULT_CONVENTIONS = buildConventions(null)
|
|
176
|
+
|
|
177
|
+
const _cache = new Map()
|
|
178
|
+
|
|
179
|
+
/** Drop the per-root cache (test seam — declaration files are read once per root). */
|
|
180
|
+
export function clearConventionsCache() {
|
|
181
|
+
_cache.clear()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Load (and cache) the normalized conventions for a project root.
|
|
186
|
+
* @param {string} cwd — project root (declaration lives at .thincoder/conventions.json)
|
|
187
|
+
* @returns {Readonly<{declared: boolean, codePaths: readonly string[],
|
|
188
|
+
* index: {codeExtensions: string[], docExtensions: string[]},
|
|
189
|
+
* advisor: {docMap: string, standardsDoc: string}}>}
|
|
190
|
+
*/
|
|
191
|
+
export function loadConventions(cwd) {
|
|
192
|
+
const root = resolve(cwd ?? process.cwd())
|
|
193
|
+
const hit = _cache.get(root)
|
|
194
|
+
if (hit) return hit
|
|
195
|
+
let conv = DEFAULT_CONVENTIONS
|
|
196
|
+
try {
|
|
197
|
+
const text = readFileSync(join(root, CONVENTIONS_REL_PATH), "utf8")
|
|
198
|
+
try {
|
|
199
|
+
const raw = JSON.parse(text)
|
|
200
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new Error("top level must be a JSON object")
|
|
201
|
+
conv = buildConventions(raw)
|
|
202
|
+
const typeErrs = typeErrorsOf(raw)
|
|
203
|
+
if (typeErrs.length > 0) {
|
|
204
|
+
// Wrong-typed keys fall back per-key — but the user must SEE that their
|
|
205
|
+
// declaration did not take effect (never silently swallowed).
|
|
206
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} has invalid value types (${typeErrs.join("; ")}) — those keys fall back to defaults`)
|
|
207
|
+
logEvent("conventions:error", { cwd: root, err: `type errors: ${typeErrs.join("; ").slice(0, 160)}` })
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
// Corrupt file / wrong shape → defaults, visible: warn + event (never silent).
|
|
211
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} unreadable (${e?.message ?? e}) — falling back to defaults`)
|
|
212
|
+
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
213
|
+
}
|
|
214
|
+
} catch (e) {
|
|
215
|
+
if (e?.code !== "ENOENT") {
|
|
216
|
+
// File exists but cannot be read (EACCES etc.) — same visible degradation.
|
|
217
|
+
console.warn(`[conventions] ${CONVENTIONS_REL_PATH} not readable (${e?.message ?? e}) — falling back to defaults`)
|
|
218
|
+
logEvent("conventions:error", { cwd: root, err: String(e?.message ?? e).slice(0, 200) })
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_cache.set(root, conv)
|
|
222
|
+
return conv
|
|
223
|
+
}
|
package/embedding.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* embedding.mjs — vector embeddings
|
|
3
|
+
* OpenAI-compatible /v1/embeddings (SiliconFlow bge-m3 / Ollama / OpenAI all supported),
|
|
4
|
+
* reuses provider.mjs fetch + retry pattern, zero dependencies.
|
|
5
|
+
* Vectors are normalized before storage; dot product then equals cosine similarity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RETRYABLE_STATUS } from "./provider/index.mjs"
|
|
9
|
+
const MAX_RETRIES = 3
|
|
10
|
+
const BATCH_SIZE = 32 // max texts per request (within SiliconFlow limits)
|
|
11
|
+
|
|
12
|
+
/** Create an embedder. config: { baseURL, apiKey, model } */
|
|
13
|
+
export function createEmbedder(config) {
|
|
14
|
+
if (!config?.baseURL) throw new Error("embedding config: baseURL is required — configure embedding.baseURL in ~/.thincoder/config.json")
|
|
15
|
+
if (!config?.apiKey) throw new Error("embedding config: apiKey is required — configure embedding.apiKey in ~/.thincoder/config.json")
|
|
16
|
+
if (!config?.model) throw new Error("embedding config: model is required — configure embedding.model in ~/.thincoder/config.json")
|
|
17
|
+
return {
|
|
18
|
+
baseURL: config.baseURL.replace(/\/+$/, ""),
|
|
19
|
+
apiKey: config.apiKey,
|
|
20
|
+
model: config.model,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Batch embedding. texts: string[] → Float32Array[] (normalized)
|
|
26
|
+
* Auto-batches, retries on failure (exponential backoff).
|
|
27
|
+
*/
|
|
28
|
+
export async function embed(embedder, texts, { signal } = {}) {
|
|
29
|
+
if (texts.length === 0) return []
|
|
30
|
+
const vectors = []
|
|
31
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
32
|
+
const batch = texts.slice(i, i + BATCH_SIZE)
|
|
33
|
+
const data = await requestWithRetry(embedder, batch, signal)
|
|
34
|
+
// Mismatched count is a hard error — silently accepting would misalign vectors with texts, poisoning the entire index
|
|
35
|
+
if (!Array.isArray(data.data) || data.data.length !== batch.length) {
|
|
36
|
+
throw new Error(`Embedding API returned ${data.data?.length ?? 0} vectors for ${batch.length} inputs`)
|
|
37
|
+
}
|
|
38
|
+
// Spec says data[] order matches input, but sort by index field if present — don't bet on server implementation
|
|
39
|
+
const items = data.data.every((d) => typeof d.index === "number")
|
|
40
|
+
? [...data.data].sort((a, b) => a.index - b.index)
|
|
41
|
+
: data.data
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
vectors.push(normalize(Float32Array.from(item.embedding)))
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return vectors
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Cosine similarity (inputs are normalized, dot product equals cosine) */
|
|
50
|
+
export function cosine(a, b) {
|
|
51
|
+
if (a.length !== b.length) return 0
|
|
52
|
+
let sum = 0
|
|
53
|
+
const n = a.length
|
|
54
|
+
for (let i = 0; i < n; i++) sum += a[i] * b[i]
|
|
55
|
+
return sum
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Float32Array → Buffer suitable for sqlite BLOB storage */
|
|
59
|
+
export function toBlob(vec) {
|
|
60
|
+
return Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** sqlite BLOB → Float32Array */
|
|
64
|
+
export function fromBlob(buf) {
|
|
65
|
+
// BLOB may come from Buffer pool where byteOffset isn't 4-aligned; creating a view directly would RangeError — copy to align first
|
|
66
|
+
if (buf.byteOffset % 4 !== 0) buf = new Uint8Array(buf)
|
|
67
|
+
if (buf.byteLength % 4 !== 0) return new Float32Array(0)
|
|
68
|
+
return new Float32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------- internal
|
|
72
|
+
|
|
73
|
+
async function requestWithRetry(embedder, input, signal) {
|
|
74
|
+
let lastError
|
|
75
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
76
|
+
if (attempt > 0) await sleep(2 ** (attempt - 1) * 1000)
|
|
77
|
+
|
|
78
|
+
let response
|
|
79
|
+
try {
|
|
80
|
+
response = await fetch(`${embedder.baseURL}/embeddings`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Bearer ${embedder.apiKey}`,
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify({ model: embedder.model, input }),
|
|
87
|
+
signal: signal
|
|
88
|
+
? AbortSignal.any([signal, AbortSignal.timeout(60_000)])
|
|
89
|
+
: AbortSignal.timeout(60_000),
|
|
90
|
+
})
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (error.name === "AbortError") throw error
|
|
93
|
+
lastError = error
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (response.ok) return response.json()
|
|
98
|
+
|
|
99
|
+
const text = await response.text().catch(() => "")
|
|
100
|
+
const message = `Embedding API error ${response.status}: ${text}`
|
|
101
|
+
if (RETRYABLE_STATUS.has(response.status)) {
|
|
102
|
+
lastError = new Error(message)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
throw new Error(message)
|
|
106
|
+
}
|
|
107
|
+
throw lastError
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function normalize(vec) {
|
|
111
|
+
let sum = 0
|
|
112
|
+
for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i]
|
|
113
|
+
const norm = Math.sqrt(sum) || 1
|
|
114
|
+
for (let i = 0; i < vec.length; i++) vec[i] /= norm
|
|
115
|
+
return vec
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sleep(ms) {
|
|
119
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
120
|
+
}
|
package/escape.mjs
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* escape.mjs — 中和 OpenAI 兼容服务端在 message content 内做的非标二次转义解析 + 孤立代理净化。
|
|
3
|
+
*
|
|
4
|
+
* 两个独立毒源(均真机实证):
|
|
5
|
+
*
|
|
6
|
+
* ① 字面 hex 转义二次解析(2026-08-06 Kimi 首观察):Kimi/deepseek 等网关会把 content 里的字面
|
|
7
|
+
* "\x5Cx" / "\x5Cu" 当作 hex escape 再解释一遍,不足位时 400("unexpected end of hex escape")。
|
|
8
|
+
* 对策:不足位序列前 double 反斜杠(\\x5CxNN 形态还原为字面量);合法完整序列放行。
|
|
9
|
+
* Known limitation(2026-09-01 v3 修复):反斜杠 run ≥3 时(如 "\\\x5Cu" 三反斜杠+u)v1 的
|
|
10
|
+
* lookbehind 只看前 1 字符会整体放行,但二次解析按配对消费后尾部的 \x5Cu 仍裸露 → 炸。
|
|
11
|
+
* 修复:按 run 奇偶判断——run 为奇数时尾部的 \x5Cu/\x5Cx 裸露(需 double),偶数已配对(放行)。
|
|
12
|
+
*
|
|
13
|
+
* ② 孤立 UTF-16 代理对(2026-09-02 deepseek 真机实锤):content 里的**真实孤立代理字符**
|
|
14
|
+
* (高代理 U+D800-DBFF 无低代理跟随,或低代理 U+DC00-DFFF 无高代理前置)——JSON.stringify
|
|
15
|
+
* 输出 \ud83d(合法 JSON),但 deepseek 解析器严格 UTF-16 解码,孤立代理 → 400
|
|
16
|
+
* ("unexpected end of hex escape" / "lone leading surrogate in hex escape")。
|
|
17
|
+
* 来源实证:doc_search 结果预览 `slice(0, N)` 按 UTF-16 码元截断,emoji 🔴(代理对)恰在
|
|
18
|
+
* 截断边界被切成孤立高代理 → 注入 system reminder → 每轮发送 → deepseek 400。
|
|
19
|
+
* 对策:发送前把孤立代理替换为 U+FFFD(任何来源安全兜底);源头截断点另修 UTF-16 安全切。
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** 中和非法字面 hex 转义序列(毒源①)。 */
|
|
23
|
+
export function escapeLiteralEscapes(text) {
|
|
24
|
+
text = String(text ?? "")
|
|
25
|
+
let out = ""
|
|
26
|
+
let i = 0
|
|
27
|
+
const n = text.length
|
|
28
|
+
while (i < n) {
|
|
29
|
+
const ch = text[i]
|
|
30
|
+
if (ch !== "\\") { out += ch; i++; continue }
|
|
31
|
+
// 数反斜杠 run 长度
|
|
32
|
+
let run = 0
|
|
33
|
+
while (i + run < n && text[i + run] === "\\") run++
|
|
34
|
+
const next = text[i + run]
|
|
35
|
+
if ((next === "x" || next === "u") && run % 2 === 1) {
|
|
36
|
+
// run 奇数 → 二次解析配对消费后尾部 \x5Cx/\x5Cu 裸露——hex 不足则网关炸 → 前插反斜杠 double
|
|
37
|
+
const need = next === "u" ? 4 : 2
|
|
38
|
+
const after = text.slice(i + run + 1, i + run + 1 + need)
|
|
39
|
+
if (!new RegExp(`^[0-9a-fA-F]{${need}}$`).test(after)) {
|
|
40
|
+
out += "\\".repeat(run + 1) + next
|
|
41
|
+
i += run + 1
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
// 合法完整:输出全序列并跳过(hex 尾不重新扫描)
|
|
45
|
+
out += text.slice(i, i + run + 1 + need)
|
|
46
|
+
i += run + 1 + need
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
out += "\\".repeat(run)
|
|
50
|
+
i += run
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 净化孤立 UTF-16 代理对(毒源②):高代理无低代理跟随 / 低代理无高代理前置 → 替换为 。 */
|
|
56
|
+
export function sanitizeLoneSurrogates(text) {
|
|
57
|
+
text = String(text ?? "")
|
|
58
|
+
let out = ""
|
|
59
|
+
let i = 0
|
|
60
|
+
const n = text.length
|
|
61
|
+
while (i < n) {
|
|
62
|
+
const cp = text.charCodeAt(i)
|
|
63
|
+
if (cp >= 0xd800 && cp <= 0xdbff) {
|
|
64
|
+
const next = text.charCodeAt(i + 1)
|
|
65
|
+
if (next >= 0xdc00 && next <= 0xdfff) { out += text[i] + text[i + 1]; i += 2; continue }
|
|
66
|
+
out += ""; i++; continue // 孤立高代理
|
|
67
|
+
}
|
|
68
|
+
if (cp >= 0xdc00 && cp <= 0xdfff) { out += ""; i++; continue } // 孤立低代理
|
|
69
|
+
out += text[i]; i++
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 发送前文本净化总入口:hex 转义中和 + 孤立代理净化。 */
|
|
75
|
+
export function sanitizeText(text) {
|
|
76
|
+
return sanitizeLoneSurrogates(escapeLiteralEscapes(text))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** 对单条消息的 content 应用 sanitizeText(支持字符串或 OpenAI 多模态 part 数组)。
|
|
80
|
+
* 2026-08-31 会诊 F5:deepseek-v4-flash 网关对 tool_calls[].function.arguments 与
|
|
81
|
+
* reasoning_content 做同样的非标二次转义解析(字面 \\x5Cx/\\x5Cu 经工具参数/思考回传 → 400,
|
|
82
|
+
* 列号确定性复现 = 毒序列在 content 之外)——这两个字符串字段同样需要中和。 */
|
|
83
|
+
export function escapeMessageContent(message) {
|
|
84
|
+
const content = message?.content
|
|
85
|
+
let changed = false
|
|
86
|
+
let next = message
|
|
87
|
+
if (typeof content === "string") {
|
|
88
|
+
const escaped = sanitizeText(content)
|
|
89
|
+
if (escaped !== content) {
|
|
90
|
+
next = { ...next, content: escaped }
|
|
91
|
+
changed = true
|
|
92
|
+
}
|
|
93
|
+
} else if (Array.isArray(content)) {
|
|
94
|
+
const parts = content.map((p) => {
|
|
95
|
+
if (p && typeof p === "object" && p.type === "text" && typeof p.text === "string") {
|
|
96
|
+
const escaped = sanitizeText(p.text)
|
|
97
|
+
if (escaped !== p.text) {
|
|
98
|
+
changed = true
|
|
99
|
+
return { ...p, text: escaped }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return p
|
|
103
|
+
})
|
|
104
|
+
if (changed) next = { ...next, content: parts }
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(next.tool_calls)) {
|
|
107
|
+
let tcChanged = false
|
|
108
|
+
const tool_calls = next.tool_calls.map((tc) => {
|
|
109
|
+
const args = tc?.function?.arguments
|
|
110
|
+
if (typeof args === "string") {
|
|
111
|
+
const escaped = sanitizeText(args)
|
|
112
|
+
if (escaped !== args) {
|
|
113
|
+
tcChanged = true
|
|
114
|
+
return { ...tc, function: { ...tc.function, arguments: escaped } }
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return tc
|
|
118
|
+
})
|
|
119
|
+
if (tcChanged) {
|
|
120
|
+
next = { ...next, tool_calls }
|
|
121
|
+
changed = true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (typeof next.reasoning_content === "string") {
|
|
125
|
+
const escaped = sanitizeText(next.reasoning_content)
|
|
126
|
+
if (escaped !== next.reasoning_content) {
|
|
127
|
+
next = { ...next, reasoning_content: escaped }
|
|
128
|
+
changed = true
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return changed ? next : message
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** IKBGX4 + SESSION.md §6.9:剥离仅本地使用的整消息标记字段(transient/ts)——发送给 provider 前移除。
|
|
135
|
+
* 严格 OpenAI 兼容服务端(opencode/LiteLLM 等)会拒绝消息级未知 key
|
|
136
|
+
* ("Extra inputs are not permitted, field: 'messages[i].transient'");ts 同理
|
|
137
|
+
* (消息时间戳是本地取证字段,不进任何 provider 请求——T-S3)。copy-on-write:
|
|
138
|
+
* 历史里的原对象不动(read_history 仍能读到 ts)。 */
|
|
139
|
+
export function stripLocalMessageFields(messages) {
|
|
140
|
+
return messages.map((m) => {
|
|
141
|
+
if (m && typeof m === "object" && ("transient" in m || "ts" in m)) {
|
|
142
|
+
const { transient, ts, ...rest } = m
|
|
143
|
+
return rest
|
|
144
|
+
}
|
|
145
|
+
return m
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 对整个 messages 数组逐条应用 escapeMessageContent(先剥离本地字段,再转义)。 */
|
|
150
|
+
export function escapeMessages(messages) {
|
|
151
|
+
return stripLocalMessageFields(messages).map(escapeMessageContent)
|
|
152
|
+
}
|
package/expand-home.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* expand-home.mjs — 配置路径字段家目录展开器(第 29 批 HOME-EXPANSION,2026-09-11)。
|
|
3
|
+
* `loadConfig()` 单一规范化点(设计 MEMORY.md §9.3a):纯函数、零依赖、home 可注入。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { homedir } from "node:os"
|
|
7
|
+
import { join } from "node:path"
|
|
8
|
+
|
|
9
|
+
/** 展开配置路径字段的前缀 `~`(`~` / `~/` / `~\`)为主目录绝对路径;不识别形态原样返回。
|
|
10
|
+
* home 第二参 = 测试注入缝(生产缺省 homedir())。 */
|
|
11
|
+
export function expandHome(p, home = homedir()) {
|
|
12
|
+
if (typeof p !== "string" || !p.startsWith("~")) return p // 非字符串 / 非 ~ 前缀 → 原样
|
|
13
|
+
if (p === "~") return home // 裸 ~ = 主目录
|
|
14
|
+
if (p[1] !== "/" && p[1] !== "\\") return p // ~user 等非分隔符 → 原样(不猜用户)
|
|
15
|
+
return join(home, p.slice(2).replaceAll("\\", "/")) // 余段分隔符归一(跨端统一)
|
|
16
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* explore-distill.mjs — End-of-run exploration distillation (AGENT-LOOP §13 +
|
|
3
|
+
* CONTEXT-COMPACTION §6.9 H1, 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. Call form = session continuation
|
|
8
|
+
* (prefix reuse · §6.15): system + `history[0, lastBlockEnd)` verbatim messages + one tail
|
|
9
|
+
* instruction — built by the single-source constructor compress-form.mjs#buildCompressMessages
|
|
10
|
+
* (no second builder · D2); the declaration face (systemPrompt / tools) rides `extras` from the
|
|
11
|
+
* call point, same source as the turn request. VS Code adapter = thincoder-vscode/src/explore-distill.mjs.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { chat } from "./provider/index.mjs"
|
|
15
|
+
import { buildCompressMessages } from "./compress-form.mjs"
|
|
16
|
+
|
|
17
|
+
/** Read-only knowledge tools counted as "exploration" (execute writes files → never exploration). */
|
|
18
|
+
export const EXPLORE_TOOLS = new Set([
|
|
19
|
+
"read", "grep", "glob", "ls", "code_search", "doc_search", "repo_outline",
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/** Summary prompt for turning a burst of exploration results into a semantic summary. */
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Requirements:
|
|
26
|
+
- Capture WHAT was discovered, WHERE (which files / directories / symbols), and the KEY CONCLUSIONS — do not list tool calls mechanically
|
|
27
|
+
- Keep actionable facts the main agent needs to continue: code locations, function names, file paths, structure, and open questions the exploration raised
|
|
28
|
+
- Drop raw tool-output noise, repeated lines, and verbatim file dumps — keep only what must be remembered
|
|
29
|
+
- Be honest: mark anything not actually verified as "unverified"; do not present guesses as facts
|
|
30
|
+
- Use bullet points; aim for information completeness, not a hard word limit
|
|
31
|
+
|
|
32
|
+
Exploration log:
|
|
33
|
+
`
|
|
34
|
+
|
|
35
|
+
/** tool_calls name across both stored shapes ({function:{name}} and flat {name}). */
|
|
36
|
+
function toolCallName(tc) {
|
|
37
|
+
return tc?.function?.name ?? tc?.name ?? ""
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Tool that produced a tool-result message (falls back to its owner assistant's tool_call). */
|
|
41
|
+
function toolResultName(msg, ownerToolCalls) {
|
|
42
|
+
if (typeof msg?.name === "string" && msg.name) return msg.name
|
|
43
|
+
const owner = (ownerToolCalls ?? []).find((tc) => tc.id === msg?.tool_call_id)
|
|
44
|
+
return owner ? toolCallName(owner) : ""
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find the pure-exploration "assistant(tool_calls)→tool…" pair blocks added since `start`.
|
|
49
|
+
* A block is explorable only when EVERY tool call AND every tool result in it is an exploration
|
|
50
|
+
* tool — mixed blocks (read + edit in one turn) stay untouched, or we'd orphan the edit pairing.
|
|
51
|
+
*/
|
|
52
|
+
function findExplorationBlocks(history, start) {
|
|
53
|
+
const blocks = []
|
|
54
|
+
let i = start
|
|
55
|
+
while (i < history.length) {
|
|
56
|
+
const m = history[i]
|
|
57
|
+
if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
58
|
+
let j = i + 1
|
|
59
|
+
while (j < history.length && history[j]?.role === "tool") j++
|
|
60
|
+
const toolMsgs = history.slice(i + 1, j)
|
|
61
|
+
const allCallsExplore = m.tool_calls.every((tc) => EXPLORE_TOOLS.has(toolCallName(tc)))
|
|
62
|
+
const allResultsExplore = toolMsgs.length > 0 && toolMsgs.every((t) => EXPLORE_TOOLS.has(toolResultName(t, m.tool_calls)))
|
|
63
|
+
if (allCallsExplore && allResultsExplore) {
|
|
64
|
+
blocks.push({ start: i, end: j, messages: history.slice(i, j), toolCount: toolMsgs.length })
|
|
65
|
+
}
|
|
66
|
+
i = j
|
|
67
|
+
} else {
|
|
68
|
+
i++
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return blocks
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Core (shared) distillation: replace this run's pure-exploration pair blocks with a single
|
|
76
|
+
* "[Exploration summary]" note placed where the first block was. Returns a NEW history array,
|
|
77
|
+
* or null when there is nothing to shrink (<3 exploration results / LLM failure). Pairing-safe:
|
|
78
|
+
* whole assistant→tool blocks are removed, so no orphan tool_calls/tool can survive.
|
|
79
|
+
*/
|
|
80
|
+
async function distillExplorations(history, start, provider, signal, agent, depth, extras) {
|
|
81
|
+
if (!Array.isArray(history) || history.length - start < 2) return null
|
|
82
|
+
const blocks = findExplorationBlocks(history, start)
|
|
83
|
+
const resultCount = blocks.reduce((n, b) => n + b.toolCount, 0)
|
|
84
|
+
if (resultCount < 3) return null
|
|
85
|
+
|
|
86
|
+
// 切点 = 本 run 末块的 `end`(与替换面同一 blocks 数组——不新增第二处切割判据)。请求前缀 =
|
|
87
|
+
// tools 声明 + system + `[0, lastBlockEnd)`——皆回合请求已建缓存面(§6.15);未命中面 =
|
|
88
|
+
// 尾部指令一条(+ ≤255 块对齐残余)。
|
|
89
|
+
const cut = blocks.at(-1).end
|
|
90
|
+
|
|
91
|
+
let summary
|
|
92
|
+
try {
|
|
93
|
+
// Silent by design (D11): thinking:null and no onToken/onReasoning — this internal
|
|
94
|
+
// distillation must not stream to the frontend. signal propagates user cancellation.
|
|
95
|
+
// 会话续写形态(§6.15 / D-CC21):messages 由单源构造器 buildCompressMessages 产出(不设第二构造
|
|
96
|
+
// 点);tools 取与回合请求同一声明面(**不带** tool_choice——实测该参数使服务端丢弃 tools 区);
|
|
97
|
+
// 不覆盖 reasoningEffort ⇒ 与回合侧同源(v3 形态)。extras 缺省 ⇒ 退化面 1(无 system / 无 tools)。
|
|
98
|
+
const resp = await chat({ ...provider, thinking: null }, {
|
|
99
|
+
messages: buildCompressMessages(history, cut, extras?.systemPrompt, EXPLORE_SUMMARY_PROMPT),
|
|
100
|
+
tools: extras?.tools,
|
|
101
|
+
signal,
|
|
102
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=distill(探索蒸馏面——agent 元数据透出;
|
|
103
|
+
// depth 经 summarizeRunExplorations 参数透传——agent.mjs 主作用域传入)
|
|
104
|
+
logCtx: {
|
|
105
|
+
stage: "distill", child: agent?._logId ?? null, kind: "distill",
|
|
106
|
+
role: agent?._role ?? null, depth: depth ?? null,
|
|
107
|
+
session: agent?._sessionStart ?? null, cwd: agent?.cwd ?? process.cwd(),
|
|
108
|
+
traces: agent?.config?.traces?.enabled !== false,
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
summary = resp?.content
|
|
112
|
+
} catch {
|
|
113
|
+
return null // N3: never block the run's return or lose history — original results stay
|
|
114
|
+
}
|
|
115
|
+
if (!summary) return null
|
|
116
|
+
|
|
117
|
+
const drop = new Set()
|
|
118
|
+
for (const b of blocks) for (let k = b.start; k < b.end; k++) drop.add(k)
|
|
119
|
+
const note = { role: "user", content: "[Exploration summary]\n" + summary }
|
|
120
|
+
const next = []
|
|
121
|
+
let inserted = false
|
|
122
|
+
for (let k = 0; k < history.length; k++) {
|
|
123
|
+
if (drop.has(k)) {
|
|
124
|
+
if (!inserted) { next.push(note); inserted = true }
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
next.push(history[k])
|
|
128
|
+
}
|
|
129
|
+
return next
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* End-of-run exploration distillation (runAgent's final return). Shrinks the MACHINE line
|
|
134
|
+
* (agent.history) only; agent._fullHistory is never touched. Triggers when this run added ≥3
|
|
135
|
+
* exploration tool results; on LLM failure it silently keeps the original history (N3).
|
|
136
|
+
* The distillation itself is silent and never streams (D11); `callbacks.onDistilled` fires
|
|
137
|
+
* ONLY after the replacement actually lands (never on no-op/failure) — callers persist the
|
|
138
|
+
* compressed session (SEND-STALL-DISTILL §2.3).
|
|
139
|
+
* `extras` = 前缀面({ systemPrompt, tools }——调用点透传、与回合请求同源 · §6.15);缺省 ⇒ 退化面 1。
|
|
140
|
+
*/
|
|
141
|
+
export async function summarizeRunExplorations(agent, callbacks, signal, depth = 0, extras) {
|
|
142
|
+
const next = await distillExplorations(agent.history, agent._runStartHistoryLen ?? 0, agent.provider, signal, agent, depth, extras)
|
|
143
|
+
if (!next) return
|
|
144
|
+
agent.history = next
|
|
145
|
+
// The machine line changed shape — the measured token baseline was for the pre-shrink context.
|
|
146
|
+
// Invalidate so the next compaction check re-estimates instead of over-counting stale history.
|
|
147
|
+
agent._lastPromptTokens = null
|
|
148
|
+
agent._usageAtLen = null
|
|
149
|
+
// The compressed machine line must reach the disk: the run's own save already happened,
|
|
150
|
+
// so without this hook the async distill would leave the session un-compressed on exit.
|
|
151
|
+
callbacks.onDistilled?.()
|
|
152
|
+
}
|