@thincoder/core 0.10.12 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/advisor/messages.mjs +4 -2
- package/advisor/notice.mjs +1 -1
- package/advisor/project-context.mjs +121 -11
- package/agent/completion.mjs +13 -1
- package/agent/digest-account.mjs +73 -46
- package/agent/dispatch-run.mjs +1 -1
- package/agent/dispatch.mjs +29 -21
- package/agent/family-tools.mjs +9 -5
- package/agent/helpers.mjs +1 -94
- package/agent/offload.mjs +88 -0
- package/agent/record-results.mjs +1 -6
- package/agent/run-stages.mjs +74 -49
- package/agent/run-start.mjs +8 -8
- package/agent/setup.mjs +6 -11
- package/agent/suspension.mjs +1 -1
- package/agent/turn-loop.mjs +35 -3
- package/agent/write-face.mjs +99 -0
- package/agent/write-gate.mjs +354 -5
- package/agent-tools/advisor.mjs +127 -11
- package/agent-tools/async-settle.mjs +2 -2
- package/agent-tools/bash-async.mjs +8 -2
- package/agent-tools/batch-closeout.mjs +206 -0
- package/agent-tools/batch-gate.mjs +287 -73
- package/agent-tools/batch-ledger-bind.mjs +183 -0
- package/agent-tools/batch-ledger-link.mjs +73 -0
- package/agent-tools/batch-lifecycle.mjs +113 -354
- package/agent-tools/batch-paths.mjs +45 -40
- package/agent-tools/batch-read.mjs +1 -1
- package/agent-tools/batch-reopen.mjs +142 -0
- package/agent-tools/batch-skeleton.mjs +283 -80
- package/agent-tools/batch-unit-test-gate.mjs +107 -0
- package/agent-tools/batch.mjs +243 -50
- package/agent-tools/context.mjs +6 -3
- package/agent-tools/design-slots.mjs +24 -24
- package/agent-tools/design-token.mjs +96 -15
- package/agent-tools/doc-check.mjs +18 -2
- package/agent-tools/eng.mjs +1 -1
- package/agent-tools/escalate-async.mjs +3 -3
- package/agent-tools/manifest.mjs +8 -5
- package/agent-tools/read-history.mjs +5 -5
- package/agent-tools/review-facts.mjs +31 -13
- package/agent-tools/spawn-gates.mjs +10 -14
- package/agent-tools/ssh-session.mjs +120 -111
- package/agent-tools/ssh-transfer.mjs +210 -0
- package/agent-tools/ssh-transport.mjs +277 -46
- package/agent-tools/subagent-actions-query.mjs +1 -1
- package/agent-tools/subagent-actions.mjs +1 -1
- package/agent-tools/subagent-async.mjs +3 -2
- package/agent-tools/subagent-scheduler.mjs +43 -9
- package/agent-tools/subagent-spawn.mjs +68 -9
- package/agent-tools/subagent.mjs +16 -14
- package/agent.mjs +1 -0
- package/client-identity.mjs +70 -0
- package/config-presets.mjs +5 -3
- package/config.mjs +19 -13
- package/context.mjs +39 -8
- package/declaration.mjs +2 -2
- package/doc-check/anchors.mjs +128 -28
- package/doc-check/index.mjs +93 -27
- package/doc-check/line-refs.mjs +112 -0
- package/doc-check/paragraph.mjs +24 -8
- package/embedding.mjs +2 -0
- package/explore-distill.mjs +1 -1
- package/generate-title.mjs +3 -2
- package/i18n.mjs +1 -1
- package/index-discover.mjs +4 -1
- package/ledger-close-proof.mjs +62 -0
- package/ledger-cmd.mjs +79 -194
- package/ledger-db.mjs +29 -28
- package/ledger-migrate.mjs +4 -4
- package/ledger-read.mjs +10 -8
- package/ledger-surface.mjs +1 -1
- package/ledger-tools.mjs +10 -10
- package/ledger-variant-notice.mjs +1 -1
- package/ledger-write-ops.mjs +373 -0
- package/ledger.mjs +2 -2
- package/manifest-discovery.mjs +2 -2
- package/manifest-schema.mjs +4 -0
- package/memory/memory-tool.mjs +5 -2
- package/memory/schema.mjs +5 -26
- package/model-specs.mjs +6 -5
- package/package.json +1 -1
- package/path-resolve.mjs +217 -0
- package/permission.mjs +4 -0
- package/prompt-overlays.mjs +5 -0
- package/prompts/advisor-design.md +9 -6
- package/prompts/advisor-round1.md +4 -3
- package/prompts/advisor-round2.md +6 -5
- package/prompts/advisor-round3.md +6 -5
- package/prompts/common.md +23 -10
- package/prompts/consult-base.md +2 -1
- package/prompts/discipline-engineering.md +29 -48
- package/prompts/discipline-normal.md +10 -5
- package/prompts/persona-eng-coder.md +8 -0
- package/prompts/persona-eng-designer.md +22 -12
- package/prompts/persona-engineering.md +81 -66
- package/prompts/persona-plan.md +2 -1
- package/prompts/persona-tester.md +55 -0
- package/provider/anthropic.mjs +12 -7
- package/provider/core.mjs +21 -5
- package/provider/errors.mjs +41 -0
- package/provider/google.mjs +25 -9
- package/provider/list-models.mjs +4 -3
- package/provider/rate.mjs +11 -3
- package/provider/responses.mjs +17 -7
- package/provider/sse.mjs +15 -4
- package/session-identity.mjs +21 -0
- package/session-lifecycle.mjs +12 -4
- package/session-slots.mjs +9 -14
- package/session.mjs +5 -0
- package/skip-dirs.mjs +48 -0
- package/team-ledger.mjs +4 -4
- package/text-budget.mjs +4 -0
- package/token-window.mjs +107 -10
- package/tool-docs/advisor.md +2 -2
- package/tool-docs/apply_patch.md +2 -0
- package/tool-docs/bash.md +4 -0
- package/tool-docs/batch.md +85 -1
- package/tool-docs/browser.md +3 -1
- package/tool-docs/code_search.md +3 -1
- package/tool-docs/consult_start.md +6 -2
- package/tool-docs/consult_stop.md +3 -1
- package/tool-docs/delete.md +3 -0
- package/tool-docs/doc_check.md +43 -8
- package/tool-docs/doc_search.md +5 -1
- package/tool-docs/edit.md +22 -1
- package/tool-docs/execute.md +7 -1
- package/tool-docs/fetch.md +3 -1
- package/tool-docs/file_ops.md +3 -1
- package/tool-docs/git.md +9 -1
- package/tool-docs/goal.md +2 -0
- package/tool-docs/grep.md +1 -1
- package/tool-docs/hashline_edit.md +2 -0
- package/tool-docs/insert_after.md +3 -1
- package/tool-docs/ledger.md +5 -5
- package/tool-docs/lint.md +1 -0
- package/tool-docs/manifest-read.md +2 -1
- package/tool-docs/manifest.md +9 -2
- package/tool-docs/memory.md +11 -1
- package/tool-docs/notify_parent.md +1 -1
- package/tool-docs/plan.md +2 -0
- package/tool-docs/question.md +1 -1
- package/tool-docs/read_history.md +15 -3
- package/tool-docs/read_image.md +3 -1
- package/tool-docs/recent_changes.md +3 -1
- package/tool-docs/repo_outline.md +3 -1
- package/tool-docs/settings.md +10 -2
- package/tool-docs/skill.md +2 -0
- package/tool-docs/ssh.md +30 -17
- package/tool-docs/subagent.md +50 -13
- package/tool-docs/task.md +2 -0
- package/tool-docs/team_ledger.md +1 -1
- package/tool-docs/timer.md +7 -0
- package/tool-docs/verify.md +13 -1
- package/tool-docs/websearch.md +3 -1
- package/tool-docs/write.md +2 -0
- package/tools/bash.mjs +12 -2
- package/tools/edit-batch.mjs +40 -33
- package/tools/edit-diff.mjs +69 -1
- package/tools/execute.mjs +10 -4
- package/tools/file.mjs +17 -27
- package/tools/git.mjs +27 -1
- package/tools/ops.mjs +2 -1
- package/tools/patch.mjs +12 -3
- package/tools/process-doc-mark.mjs +139 -0
- package/tools/search.mjs +15 -6
- package/tools/shared.mjs +24 -1
- package/tools/ssh.mjs +48 -7
- package/tools/tree.mjs +4 -1
- package/tools/write-path.mjs +64 -3
- package/traces/trace-store.mjs +4 -3
- package/agent/light-round.mjs +0 -63
- package/agent/unbooked-pen.mjs +0 -62
package/agent/family-tools.mjs
CHANGED
|
@@ -45,17 +45,21 @@ export async function assembleFamilyTools({
|
|
|
45
45
|
return { ...tool, description: tool.description + `\nCurrently configured consultants (this tool's pool): ${list}` }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// Role enum is mutually exclusive: normal mode has "coder", engineering mode has "eng-coder"/"eng-designer"
|
|
48
|
+
// Role enum is mutually exclusive: normal mode has "coder", engineering mode has "eng-coder"/"eng-designer";
|
|
49
|
+
// "tester" (AGENT-LOOP-SUBAGENT.md §6.59) is dispatchable in both modes — it runs verification and returns
|
|
50
|
+
// readings; its tool table (read-only set ∪ execute/bash − {verify, advisor}) holds no write tool. Run-only is the
|
|
51
|
+
// role contract, not a sandbox (§6.59.7 边界 1 — `bash` can still write, and that is by design).
|
|
52
|
+
const testerSuffix = " role='tester' runs verification and reports the readings back — it never edits code or documents."
|
|
49
53
|
const subagentRoles = (depth === 0 && engineering)
|
|
50
54
|
? {
|
|
51
|
-
enum: ["explore", "eng-designer", "eng-coder"],
|
|
55
|
+
enum: ["explore", "eng-designer", "eng-coder", "tester"],
|
|
52
56
|
description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required.",
|
|
53
|
-
suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled) and role='eng-designer' for writing the requirements/design documents.",
|
|
57
|
+
suffix: " In engineering mode, use role='eng-coder' for implementation (coder is disabled) and role='eng-designer' for writing the requirements/design documents." + testerSuffix,
|
|
54
58
|
}
|
|
55
59
|
: {
|
|
56
|
-
enum: ["explore", "plan", "coder"],
|
|
60
|
+
enum: ["explore", "plan", "coder", "tester"],
|
|
57
61
|
description: "The sub-agent role — see the tool description for the role capability matrix. Exact spelling required.",
|
|
58
|
-
suffix:
|
|
62
|
+
suffix: testerSuffix,
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
const filteredSubagent = depth === 0 ? {
|
package/agent/helpers.mjs
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* agent/helpers.mjs — Agent utility functions and constants
|
|
3
3
|
*/
|
|
4
|
-
import { configDir } from "../config.mjs"
|
|
5
4
|
import { readFileSync, readdirSync } from "node:fs"
|
|
6
5
|
import { homedir } from "node:os"
|
|
7
|
-
import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
8
6
|
import { join } from "node:path"
|
|
9
7
|
import { execFile } from "node:child_process"
|
|
10
8
|
import { promisify } from "node:util"
|
|
9
|
+
export { offloadToolResult, cleanupOldToolResults, TMP_RETENTION_MS } from "./offload.mjs"
|
|
11
10
|
|
|
12
11
|
/** Single source for the AUTO-mode reminder (was duplicated in agent.mjs +
|
|
13
12
|
* setup.mjs — consult P2, 2026-08-30). */
|
|
@@ -33,52 +32,6 @@ export const REPORT_CONTINUATION =
|
|
|
33
32
|
"3. How you verified (tests run, commands executed, with results)\n" +
|
|
34
33
|
"4. Anything left undone or worth follow-up"
|
|
35
34
|
|
|
36
|
-
const TOOL_RESULT_OFFLOAD_LIMIT = 64 * 1024 // 65536 chars — offload only above 64K (2026-08-24)
|
|
37
|
-
const TOOL_RESULT_PREVIEW = 64 * 1024 // total preview budget: head + middle note + tail ≤ 65536 (aligns with CLI/VS Code webview)
|
|
38
|
-
const TOOL_RESULT_PREVIEW_HEAD = 16 * 1024 // head slice preserved (2026-09-04 §5 — dual-end preview)
|
|
39
|
-
const TOOL_RESULT_PREVIEW_TAIL = 48 * 1024 // nominal tail slice (results/errors/stats live here — actual tail = budget remainder, see buildDualEndPreview)
|
|
40
|
-
|
|
41
|
-
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
42
|
-
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
43
|
-
* "unexpected end of hex escape"。截断点落在高代理上时向前收一个码元。
|
|
44
|
-
* 与 setup.mjs 的 safeSliceUTF16 同语义(两处独立实现——escape.mjs 的 sanitizeLoneSurrogates 是发送兜底,此处是源头)。 */
|
|
45
|
-
function safeSliceUTF16(text, max) {
|
|
46
|
-
if (text.length <= max) return text
|
|
47
|
-
const cp = text.charCodeAt(max - 1)
|
|
48
|
-
if (cp >= 0xd800 && cp <= 0xdbff) return text.slice(0, max - 1)
|
|
49
|
-
return text.slice(0, max)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** UTF-16 safe END slice (2026-09-04 §5 dual-end preview — review #5: both boundaries must not split
|
|
53
|
-
* a surrogate pair). Same rule as safeSliceUTF16, mirrored: if the slice START lands on a LOW
|
|
54
|
-
* surrogate (DC00-DFFF — the second half of a pair whose high half sits just before the boundary),
|
|
55
|
-
* advance one code unit so the slice never begins with an orphan low surrogate. */
|
|
56
|
-
function safeSliceUTF16End(text, max) {
|
|
57
|
-
if (text.length <= max) return text
|
|
58
|
-
const start = text.length - max
|
|
59
|
-
const cp = text.charCodeAt(start)
|
|
60
|
-
if (cp >= 0xdc00 && cp <= 0xdfff) return text.slice(start + 1)
|
|
61
|
-
return text.slice(start)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Dual-end preview (design §5 D-4.1): head + middle-omitted note + tail — the tail carries
|
|
65
|
-
* results/errors/stats that a pure-head truncation would cut off.
|
|
66
|
-
* Budget (round1 review #2, fixed): head + note + tail ≤ TOOL_RESULT_PREVIEW (65536) — tail is
|
|
67
|
-
* computed from constants (tail = TOOL_RESULT_PREVIEW − head − noteLen), never hardcoded.
|
|
68
|
-
* The note length depends on the omitted digit count; text.length's digit count is an upper bound
|
|
69
|
-
* for omitted (< text.length), so budgeting with it keeps the total ≤ 65536 while the printed
|
|
70
|
-
* note reports the actual omitted count. Both boundaries run surrogate-safe slices (review #5). */
|
|
71
|
-
function buildDualEndPreview(text) {
|
|
72
|
-
const head = safeSliceUTF16(text, TOOL_RESULT_PREVIEW_HEAD)
|
|
73
|
-
const noteFn = (omitted) => `\n\n… [middle omitted: ${omitted} chars] …\n\n`
|
|
74
|
-
const tailLen = Math.min(TOOL_RESULT_PREVIEW_TAIL, TOOL_RESULT_PREVIEW - TOOL_RESULT_PREVIEW_HEAD - noteFn(text.length).length)
|
|
75
|
-
const tail = safeSliceUTF16End(text, tailLen)
|
|
76
|
-
return head + noteFn(Math.max(0, text.length - head.length - tail.length)) + tail
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
|
|
80
|
-
export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
|
|
81
|
-
|
|
82
35
|
const GIT_TIMEOUT_MS = 5000
|
|
83
36
|
const MAX_GIT_CHANGES_DISPLAY = 20
|
|
84
37
|
|
|
@@ -127,52 +80,6 @@ export function tryCanonicalize(name, args) {
|
|
|
127
80
|
try { return name + ":" + JSON.stringify(JSON.parse(args)) } catch { return name + ":" + args }
|
|
128
81
|
}
|
|
129
82
|
|
|
130
|
-
/**
|
|
131
|
-
* Best-effort write-time self-cleanup: delete files in dir whose mtime exceeds TMP_RETENTION_MS.
|
|
132
|
-
* Subdirectories are never touched; every failure is silent — cleanup must not affect offload.
|
|
133
|
-
*/
|
|
134
|
-
export async function cleanupOldToolResults(dir) {
|
|
135
|
-
const now = Date.now()
|
|
136
|
-
let entries
|
|
137
|
-
try {
|
|
138
|
-
entries = await readdir(dir, { withFileTypes: true })
|
|
139
|
-
} catch {
|
|
140
|
-
return // dir missing or unreadable → nothing to clean
|
|
141
|
-
}
|
|
142
|
-
for (const entry of entries) {
|
|
143
|
-
if (!entry.isFile()) continue // subdirectories untouched
|
|
144
|
-
try {
|
|
145
|
-
const st = await stat(join(dir, entry.name))
|
|
146
|
-
if (now - st.mtimeMs > TMP_RETENTION_MS) await unlink(join(dir, entry.name))
|
|
147
|
-
} catch {
|
|
148
|
-
/* entry vanished concurrently or I/O error — best effort, keep going */
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** Offload a tool result / report to disk (§2.2): `force` = write regardless of size (async
|
|
154
|
-
* subagent reports must survive the parent turn). Rendering splits on the SAME constant as the old
|
|
155
|
-
* gate: ≤ TOOL_RESULT_OFFLOAD_LIMIT ⇒ body once + path (never dual-end — a short body would come
|
|
156
|
-
* out twice with `middle omitted: 0 chars`); > ⇒ dual-end preview + path. Writes self-clean first. */
|
|
157
|
-
export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results"), { force = false } = {}) {
|
|
158
|
-
const overLimit = text.length > TOOL_RESULT_OFFLOAD_LIMIT
|
|
159
|
-
if (!force && !overLimit) return text
|
|
160
|
-
try {
|
|
161
|
-
await cleanupOldToolResults(dir)
|
|
162
|
-
await mkdir(dir, { recursive: true })
|
|
163
|
-
const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
164
|
-
await writeFile(file, text, "utf8")
|
|
165
|
-
if (!overLimit) return `${text}\n\n[... full content saved to: ${file}\nPage through it with the read tool (offset/limit) if you need the full text later.]`
|
|
166
|
-
return buildDualEndPreview(text) + `\n\n[... output too large (${text.length} chars total), full content saved to: ${file}\n` +
|
|
167
|
-
`Page through it with the read tool (offset/limit) or sed -n 'START,ENDp' — do NOT re-run the tool blindly.]`
|
|
168
|
-
} catch {
|
|
169
|
-
// 写盘失败不吞报告:>LIMIT 走双端预览 + 原文案;≤LIMIT 正文整段在前(不谎称 truncated)
|
|
170
|
-
const body = overLimit ? buildDualEndPreview(text) : text
|
|
171
|
-
return body + (overLimit ? `\n\n[... truncated: ${text.length} chars total, offload to disk failed]`
|
|
172
|
-
: `\n\n[... full text kept inline above (${text.length} chars), offload to disk failed]`)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
83
|
// 失败冷却 v1(用户裁——GIT-ASYNC L21):真失败/超时后 30s 内跳过该 cwd 的收集——
|
|
177
84
|
// 病态 repo 周期拖慢变一次性。VSC setup-reminders.mjs 同构镜像。
|
|
178
85
|
const GIT_FAILURE_COOLDOWN_MS = 30_000
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent/offload.mjs — 一次「把超长正文落到盘上、把注入体压成预览 + 路径」:门限常量 → 两端安全切片 → 预览拼装 → 落盘 → 写时自清理。
|
|
3
|
+
*/
|
|
4
|
+
import { configDir } from "../config.mjs"
|
|
5
|
+
import { writeFile, mkdir, readdir, stat, unlink } from "node:fs/promises"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import { safeSliceUTF16 } from "../text-budget.mjs"
|
|
8
|
+
|
|
9
|
+
const TOOL_RESULT_OFFLOAD_LIMIT = 64 * 1024 // 65536 chars — offload only above 64K (2026-08-24)
|
|
10
|
+
const TOOL_RESULT_PREVIEW = 64 * 1024 // total preview budget: head + middle note + tail ≤ 65536 (aligns with CLI/VS Code webview)
|
|
11
|
+
const TOOL_RESULT_PREVIEW_HEAD = 16 * 1024 // head slice preserved (2026-09-04 §5 — dual-end preview)
|
|
12
|
+
const TOOL_RESULT_PREVIEW_TAIL = 48 * 1024 // nominal tail slice (results/errors/stats live here — actual tail = budget remainder, see buildDualEndPreview)
|
|
13
|
+
|
|
14
|
+
/** UTF-16 safe END slice (2026-09-04 §5 dual-end preview — review #5: both boundaries must not split
|
|
15
|
+
* a surrogate pair). Same rule as safeSliceUTF16, mirrored: if the slice START lands on a LOW
|
|
16
|
+
* surrogate (DC00-DFFF — the second half of a pair whose high half sits just before the boundary),
|
|
17
|
+
* advance one code unit so the slice never begins with an orphan low surrogate. */
|
|
18
|
+
function safeSliceUTF16End(text, max) {
|
|
19
|
+
if (text.length <= max) return text
|
|
20
|
+
const start = text.length - max
|
|
21
|
+
const cp = text.charCodeAt(start)
|
|
22
|
+
if (cp >= 0xdc00 && cp <= 0xdfff) return text.slice(start + 1)
|
|
23
|
+
return text.slice(start)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Dual-end preview (design §5 D-4.1): head + middle-omitted note + tail — the tail carries
|
|
27
|
+
* results/errors/stats that a pure-head truncation would cut off.
|
|
28
|
+
* Budget (round1 review #2, fixed): head + note + tail ≤ TOOL_RESULT_PREVIEW (65536) — tail is
|
|
29
|
+
* computed from constants (tail = TOOL_RESULT_PREVIEW − head − noteLen), never hardcoded.
|
|
30
|
+
* The note length depends on the omitted digit count; text.length's digit count is an upper bound
|
|
31
|
+
* for omitted (< text.length), so budgeting with it keeps the total ≤ 65536 while the printed
|
|
32
|
+
* note reports the actual omitted count. Both boundaries run surrogate-safe slices (review #5). */
|
|
33
|
+
function buildDualEndPreview(text) {
|
|
34
|
+
const head = safeSliceUTF16(text, TOOL_RESULT_PREVIEW_HEAD)
|
|
35
|
+
const noteFn = (omitted) => `\n\n… [middle omitted: ${omitted} chars] …\n\n`
|
|
36
|
+
const tailLen = Math.min(TOOL_RESULT_PREVIEW_TAIL, TOOL_RESULT_PREVIEW - TOOL_RESULT_PREVIEW_HEAD - noteFn(text.length).length)
|
|
37
|
+
const tail = safeSliceUTF16End(text, tailLen)
|
|
38
|
+
return head + noteFn(Math.max(0, text.length - head.length - tail.length)) + tail
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Offload-dir write-time self-cleanup retention window (2026-08-21): files older than 3 days are deleted on the next offload. */
|
|
42
|
+
export const TMP_RETENTION_MS = 3 * 24 * 3600 * 1000
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Best-effort write-time self-cleanup: delete files in dir whose mtime exceeds TMP_RETENTION_MS.
|
|
46
|
+
* Subdirectories are never touched; every failure is silent — cleanup must not affect offload.
|
|
47
|
+
*/
|
|
48
|
+
export async function cleanupOldToolResults(dir) {
|
|
49
|
+
const now = Date.now()
|
|
50
|
+
let entries
|
|
51
|
+
try {
|
|
52
|
+
entries = await readdir(dir, { withFileTypes: true })
|
|
53
|
+
} catch {
|
|
54
|
+
return // dir missing or unreadable → nothing to clean
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isFile()) continue // subdirectories untouched
|
|
58
|
+
try {
|
|
59
|
+
const st = await stat(join(dir, entry.name))
|
|
60
|
+
if (now - st.mtimeMs > TMP_RETENTION_MS) await unlink(join(dir, entry.name))
|
|
61
|
+
} catch {
|
|
62
|
+
/* entry vanished concurrently or I/O error — best effort, keep going */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Offload a tool result / report to disk (§2.2): `force` = write regardless of size (async
|
|
68
|
+
* subagent reports must survive the parent turn). Rendering splits on the SAME constant as the old
|
|
69
|
+
* gate: ≤ TOOL_RESULT_OFFLOAD_LIMIT ⇒ body once + path (never dual-end — a short body would come
|
|
70
|
+
* out twice with `middle omitted: 0 chars`); > ⇒ dual-end preview + path. Writes self-clean first. */
|
|
71
|
+
export async function offloadToolResult(text, callId, dir = join(configDir, "tool-results"), { force = false } = {}) {
|
|
72
|
+
const overLimit = text.length > TOOL_RESULT_OFFLOAD_LIMIT
|
|
73
|
+
if (!force && !overLimit) return text
|
|
74
|
+
try {
|
|
75
|
+
await cleanupOldToolResults(dir)
|
|
76
|
+
await mkdir(dir, { recursive: true })
|
|
77
|
+
const file = join(dir, `${Date.now()}-${String(callId).replace(/[^a-zA-Z0-9_-]/g, "_")}.log`)
|
|
78
|
+
await writeFile(file, text, "utf8")
|
|
79
|
+
if (!overLimit) return `${text}\n\n[... full content saved to: ${file}\nPage through it with the read tool (offset/limit) if you need the full text later.]`
|
|
80
|
+
return buildDualEndPreview(text) + `\n\n[... output too large (${text.length} chars total), full content saved to: ${file}\n` +
|
|
81
|
+
`Page through it with the read tool (offset/limit) or sed -n 'START,ENDp' — do NOT re-run the tool blindly.]`
|
|
82
|
+
} catch {
|
|
83
|
+
// 写盘失败不吞报告:>LIMIT 走双端预览 + 原文案;≤LIMIT 正文整段在前(不谎称 truncated)
|
|
84
|
+
const body = overLimit ? buildDualEndPreview(text) : text
|
|
85
|
+
return body + (overLimit ? `\n\n[... truncated: ${text.length} chars total, offload to disk failed]`
|
|
86
|
+
: `\n\n[... full text kept inline above (${text.length} chars), offload to disk failed]`)
|
|
87
|
+
}
|
|
88
|
+
}
|
package/agent/record-results.mjs
CHANGED
|
@@ -26,8 +26,6 @@
|
|
|
26
26
|
import { pushReal } from "../context.mjs"
|
|
27
27
|
import { specForModel } from "../config.mjs"
|
|
28
28
|
import { FILE_MUTATORS, toolTouchPaths } from "./helpers.mjs"
|
|
29
|
-
// F-LC6(LIGHT-CHANNEL §2.9 · 台账 #1753):无账之笔提醒叶(谓词 + 文案 + 闩单源)。
|
|
30
|
-
import { unbookedPenNotice } from "./unbooked-pen.mjs"
|
|
31
29
|
import { capHistoryImages } from "../provider/normalize.mjs"
|
|
32
30
|
import { resolve } from "node:path"
|
|
33
31
|
import { advisorRuns, stripApprovedSuffix } from "../agent-tools/advisor-async.mjs"
|
|
@@ -84,10 +82,7 @@ export async function recordToolResults(agent, toolByName, results) {
|
|
|
84
82
|
}
|
|
85
83
|
} catch { /* Parse failure doesn't affect normal tool messages */ }
|
|
86
84
|
}
|
|
87
|
-
|
|
88
|
-
// (零拦截——只拼回执文本、入史时机不变;命中后本会话不再提示)。
|
|
89
|
-
const penNotice = unbookedPenNotice(agent, toolCall, ok, tool)
|
|
90
|
-
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: penNotice ? result + penNotice : result })
|
|
85
|
+
pushReal(agent, { role: "tool", tool_call_id: toolCall.id, name: toolCall.name, content: result })
|
|
91
86
|
if (tool && ok) {
|
|
92
87
|
if (FILE_MUTATORS.has(toolCall.name)) {
|
|
93
88
|
// Direct file edit — code was changed. The prior advisor review and
|
package/agent/run-stages.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* run-stages.mjs — runAgent 主循环的阶段函数族(CLI,2026-09-05 实践轮:agent.mjs
|
|
3
3
|
* runAgent 383 ≥300 按骨干—细节两层提取——VS Code 端同构文件(双端 parity——拆分
|
|
4
|
-
* 两端同步))。本文件承载:回合压缩检查(runCompactionCheck
|
|
4
|
+
* 两端同步))。本文件承载:回合压缩检查(runCompactionCheck)、越窗守卫 / 自适应共用原子
|
|
5
|
+
* (enforceContextFit——§6.17 守卫与 §6.17.1 链三共用一个压缩原子)、回合注入组
|
|
5
6
|
* (injectTurnReminders——plan cadence + eng 状态)、回合收尾(finalizeAgentTurn——
|
|
6
7
|
* consult 清理/async 池分流/guard 继承)与其收集器(collectSettledAsync——自
|
|
7
8
|
* agent.mjs 迁入,agent.mjs 内无其他引用)。verbatim 迁移 + 签名化,语义零变。
|
|
@@ -11,6 +12,7 @@ import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT } from "../c
|
|
|
11
12
|
// §6.17/D-CC32 溢出守卫(兜底,非主修):预算面单源 requestBudget ∥ 再估面 contextUsage(与压缩判定同一函数)
|
|
12
13
|
import { requestBudget } from "../config.mjs"
|
|
13
14
|
import { contextUsage } from "../token-window.mjs"
|
|
15
|
+
import { contextOverflowError } from "../provider/errors.mjs"
|
|
14
16
|
import { ensureAutoReminder, injectEngineeringReminder, ContinueError, snapshotGuard } from "./helpers.mjs"
|
|
15
17
|
import { pushManifestStateReminder } from "./setup-reminders.mjs"
|
|
16
18
|
import { cleanupConsultSessions } from "../agent-tools/consult.mjs"
|
|
@@ -72,6 +74,35 @@ export function injectResponseReminders(agent, response) {
|
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// ─── 压缩记账三件(§6.17 阈值面 ∥ 守卫面 ∥ §6.17.1 链三自适应面共用同一段,零复制)───
|
|
78
|
+
|
|
79
|
+
/** 压缩成功记账:失败计数清零 / 计划提醒 cadence 复位 / 停滞检测计数复位 / 完成事件 / AUTO 提醒重注入。 */
|
|
80
|
+
function bookkeepCompaction(agent, ctx) {
|
|
81
|
+
agent._compressFailures = 0
|
|
82
|
+
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
83
|
+
ctx.recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
84
|
+
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
|
|
85
|
+
// the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
|
|
86
|
+
ctx.callbacks?.onCompress?.(agent._lastCompressInfo ?? {})
|
|
87
|
+
ensureAutoReminder(agent)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 压缩失败记账——AbortError 必须透传(用户中止不得被当成压缩失败吞掉)。 */
|
|
91
|
+
function noteCompressFailure(agent, ctx, compressError) {
|
|
92
|
+
if (compressError?.name === "AbortError" || ctx.signal?.aborted) throw compressError
|
|
93
|
+
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
94
|
+
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
95
|
+
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
96
|
+
ctx.callbacks?.onCompressFail?.(compressError)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** 三连败降级(确定性截断——零网络,§6.5 D6)。 */
|
|
100
|
+
function degradeIfCompressExhausted(agent, ctx) {
|
|
101
|
+
if (agent._compressFailures < COMPRESS_FAILURE_LIMIT) return
|
|
102
|
+
agent._compressFailures = 0
|
|
103
|
+
if (compressFallback(agent)) ctx.callbacks?.onCompress?.(agent._lastCompressInfo ?? {})
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
/**
|
|
76
107
|
* 回合压缩检查(2026-09-05 实践轮——自 runAgent 主循环提取):仅安全点(history 以
|
|
77
108
|
* 完整交换结尾——user/tool 位)。成功:失败计数清零/计划提醒 cadence 复位/停滞检测
|
|
@@ -81,22 +112,12 @@ export function injectResponseReminders(agent, response) {
|
|
|
81
112
|
* 模型主动压缩(`context` 工具)由本点消费(§6.16.2——取用即清槽;no-op 与失败各落一行注记)。
|
|
82
113
|
*/
|
|
83
114
|
export async function runCompactionCheck(agent, ctx) {
|
|
84
|
-
const { threshold, callbacks, compactionOverhead, signal
|
|
115
|
+
const { threshold, callbacks, compactionOverhead, signal } = ctx
|
|
85
116
|
// F-CC2(CONTEXT-COMPACTION.md §6.16.2):模型主动压缩(`context` 工具)在**本安全点**消费——
|
|
86
117
|
// 取用即清槽(失败不重放;重发由模型再调);`force` 只跳过阈值早退(模型判定优先于阈值),
|
|
87
118
|
// 其余全同(同 splitHistory / 同尾部预算 / 同摘要链 / 同回注 / 同基线失效)。
|
|
88
119
|
const pending = agent._pendingCompact
|
|
89
120
|
agent._pendingCompact = null
|
|
90
|
-
// 成功记账(§6.17:成功记账段抽局部——阈值面与守卫面共用同一段,零复制)
|
|
91
|
-
const bookkeepCompaction = () => {
|
|
92
|
-
agent._compressFailures = 0
|
|
93
|
-
agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
|
|
94
|
-
recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
|
|
95
|
-
// Completion info (CONTEXT-COMPACTION §7 D-C2): { mode, tokensFreed, elapsedMs } —
|
|
96
|
-
// the TUI panel renders it; callers that ignore the arg keep prior onCompress semantics.
|
|
97
|
-
callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
98
|
-
ensureAutoReminder(agent)
|
|
99
|
-
}
|
|
100
121
|
try {
|
|
101
122
|
let compacted = await compressIfNeeded(
|
|
102
123
|
agent, threshold, callbacks,
|
|
@@ -109,53 +130,57 @@ export async function runCompactionCheck(agent, ctx) {
|
|
|
109
130
|
agent.history.push({ role: "user", content: compactNoopNote(), transient: true })
|
|
110
131
|
compacted = await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)
|
|
111
132
|
}
|
|
112
|
-
if (compacted) bookkeepCompaction()
|
|
133
|
+
if (compacted) bookkeepCompaction(agent, ctx)
|
|
113
134
|
} catch (compressError) {
|
|
114
|
-
|
|
115
|
-
if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
|
|
116
|
-
agent._compressFailures = (agent._compressFailures ?? 0) + 1
|
|
117
|
-
// Q3 (CONTEXT-COMPACTION §7 D-C1): a failed compression is surfaced to the panel;
|
|
118
|
-
// COMPRESS_FAILURE_LIMIT consecutive failures still degrade to compressFallback.
|
|
119
|
-
callbacks?.onCompressFail?.(compressError)
|
|
135
|
+
noteCompressFailure(agent, ctx, compressError)
|
|
120
136
|
if (pending) {
|
|
121
137
|
// 失败注记(§6.16.2 回执面②):模型面告知缺口——既有失败链 / 面板面零改,仅补一行机器注记
|
|
122
138
|
const { compactFailureNote } = await import("../agent-tools/context.mjs")
|
|
123
139
|
agent.history.push({ role: "user", content: compactFailureNote(errText(compressError), agent._compressFailures), transient: true })
|
|
124
140
|
}
|
|
125
|
-
|
|
126
|
-
agent._compressFailures = 0
|
|
127
|
-
if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
|
|
128
|
-
}
|
|
141
|
+
degradeIfCompressExhausted(agent, ctx)
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
// 溢出守卫(CONTEXT-COMPACTION.md §6.17 / D-CC32——兜底,非主修;本缝 = catch 闭合后、返回前)。
|
|
132
|
-
// 判据
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
// 判据 / 强压 / 再判 / 抛三实数 全住 `enforceContextFit`——与 §6.17.1 链三的自适应面**同一个原子**(零复制)。
|
|
146
|
+
await enforceContextFit(agent, ctx)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 越窗守卫 / 自适应共用原子(CONTEXT-COMPACTION.md §6.17 守卫 ∥ §6.17.1 链三自适应):
|
|
151
|
+
* `requestBudget` 判 → 强压一次(`force`)→ 再判 → 抛三实数(不截断、不静默削输出)。
|
|
152
|
+
*
|
|
153
|
+
* `opts.forceCompact`(链三面):**跳过前置估算判据**、直接强压——服务端已判越窗时估算准不准不再作门
|
|
154
|
+
* (本批要治的就是估算低估:死锁现场估 491K < limit 606,784,按估算开门会压 0 次、重试同一请求再吃 400)。
|
|
155
|
+
* 压后仍超的再判两面同判(压后仍越线说明压缩解不了,再压只是重复付费)。
|
|
156
|
+
* 返回是否走到了再判通过(守卫面估算未越线 ⇒ false、零动作;压后仍超⇒抛,不到达返回值)。
|
|
157
|
+
*/
|
|
158
|
+
export async function enforceContextFit(agent, ctx, { forceCompact = false } = {}) {
|
|
159
|
+
const { threshold, callbacks, compactionOverhead, signal } = ctx
|
|
160
|
+
// 判据 = prompt 估算 > `budget.limit`(**无档次门**——absent 档 limit 取地板 max(window − reserve, threshold),
|
|
161
|
+
// 恒为正数、恒可判)。**判据不用 maxTokens**:未发时为 undefined ⇒ NaN ⇒ 守卫静默失效(§6.17)。
|
|
136
162
|
const budget = requestBudget(agent.provider)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
163
|
+
let usage = contextUsage(agent, compactionOverhead)
|
|
164
|
+
if (!forceCompact && !(usage.total > budget.limit)) return false
|
|
165
|
+
// 强压一次:失败走与阈值面同一条记账 / 降级链(不把摘要错误当回合错误直抛——
|
|
166
|
+
// 记账后仍按「压后仍超」判:降级截断可能已把上下文压到线下)。
|
|
167
|
+
try {
|
|
168
|
+
const compacted = await compressIfNeeded(
|
|
169
|
+
agent, threshold, callbacks, { ...compactionOverhead, force: true }, signal,
|
|
170
|
+
)
|
|
171
|
+
if (compacted) bookkeepCompaction(agent, ctx)
|
|
172
|
+
} catch (compressError) {
|
|
173
|
+
noteCompressFailure(agent, ctx, compressError)
|
|
174
|
+
degradeIfCompressExhausted(agent, ctx)
|
|
175
|
+
}
|
|
176
|
+
usage = contextUsage(agent, compactionOverhead)
|
|
177
|
+
if (usage.total > budget.limit) {
|
|
178
|
+
const remedy = budget.limit < 0
|
|
179
|
+
? `max_tokens ${budget.maxTokens} alone exceeds the window — lower provider.maxTokens or switch to a larger-window model`
|
|
180
|
+
: "forced compaction ran but the prompt still exceeds the window; reduce the conversation (/compact or prune) before retrying"
|
|
181
|
+
throw contextOverflowError(agent.provider, usage.total, budget, remedy)
|
|
158
182
|
}
|
|
183
|
+
return true
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
/**
|
|
@@ -278,7 +303,7 @@ export async function finalizeAgentTurn(agent, ctx) {
|
|
|
278
303
|
// 恒达窗 = 正常 ∥ Abort-continue ∥ 异常抛三径同达;会话停止径不判(清场语义归 finishSuspension)。
|
|
279
304
|
if (isDigestRound({ autoTurn, upstreamTurn, timerTurn }) && carrierField(agent, "_daSession") === true
|
|
280
305
|
&& !(signal?.aborted && !signal?.reason?.interrupt)) {
|
|
281
|
-
accountDigestRound(agent)
|
|
306
|
+
await accountDigestRound(agent) // async(§6.31.5 升级支 ④ 终局披露复用注入器)
|
|
282
307
|
}
|
|
283
308
|
agent._inAutoTurn = false
|
|
284
309
|
// AGENT-LOOP-ASYNC-POOL.md §6.8 D-S6: auto-turn guard marks survive into the next USER run (restored at its
|
package/agent/run-start.mjs
CHANGED
|
@@ -31,31 +31,31 @@ export async function beginRun(agent, input, callbacks, {
|
|
|
31
31
|
// (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 CONSULTATION.md §6.2 D-R17a/b)
|
|
32
32
|
// ——单容器一处清,不再逐族三段。
|
|
33
33
|
// §6.31 消化账务(批 digest-accounting · 2026-10-05 · 台账 #930):**投递 ≠ 销账**——
|
|
34
|
-
// 会话态(`_daSession`)投递留容器(覆盖才离容器 /
|
|
34
|
+
// 会话态(`_daSession`)投递留容器(覆盖才离容器 / 未见账计一个未签收轮、达 `DA_MISS_LIMIT` 升级——
|
|
35
|
+
// 投递恒一次,无重投回路:批 digest-retry-amplify · 台账 #1850);fallback
|
|
35
36
|
// (headless / 直连 runAgent)缺省条逐字沿用取尽语义(先离容后注入 ∥ `releaseSettledEntry` 面;
|
|
36
37
|
// 零账目动作——不销账 / 不重投),已投递未销账条同判据跳过且留容器(§6.31 出口缝② · 父侧裁定 2026-10-05)。
|
|
37
38
|
const pendingAsync = agent._pendingAsyncResults
|
|
38
39
|
const { carrierField, releaseChildHold, releaseSettledEntry } = await import("../agent-tools/async-settle.mjs")
|
|
39
40
|
if (carrierField(agent, "_daSession") === true) {
|
|
40
41
|
const { armAccountRound, isDigestRound, shouldDeliverEntry, digestAccountRequirement } = await import("./digest-account.mjs")
|
|
41
|
-
armAccountRound(agent) // §6.31.5 落痕清位:会话态起跑即清(缺痕 ⇒
|
|
42
|
+
armAccountRound(agent) // §6.31.5 落痕清位:会话态起跑即清(缺痕 ⇒ 该轮按未签收判一个轮次)
|
|
42
43
|
if (pendingAsync?.length) {
|
|
43
44
|
const { injectAsyncResult } = await import("../agent-tools/subagent.mjs")
|
|
44
45
|
const { injectConsultResult } = await import("../agent-tools/consult.mjs")
|
|
45
46
|
const digestRound = isDigestRound({ autoTurn, upstreamTurn, timerTurn })
|
|
46
|
-
//
|
|
47
|
-
//
|
|
47
|
+
// 投递门控单门(§6.31.4 · 投递恒一次):首投**仅系统轮**(`autoTurn`——用户回合零注入),
|
|
48
|
+
// 已投递条恒跳过;注入后**留容器**(不 splice)。
|
|
48
49
|
// 注入抛错 ⇒ 未投条目(`_daDelivered` 未置)留容器待下轮——收窄既有「splice 先取尽」的丢失窗。
|
|
49
50
|
for (const e of [...pendingAsync]) {
|
|
50
|
-
if (!shouldDeliverEntry(e,
|
|
51
|
+
if (!shouldDeliverEntry(e, autoTurn)) continue
|
|
51
52
|
if (e.role === "consult") await injectConsultResult(agent, e)
|
|
52
53
|
else await injectAsyncResult(agent, e)
|
|
53
54
|
e._daDelivered = true
|
|
54
|
-
e
|
|
55
|
-
releaseChildHold(e) // 仅释放 `childAgent`——`report` 保留至销账 / 升级(重投需原文)
|
|
55
|
+
releaseChildHold(e) // 仅释放 `childAgent`——`report` 保留至销账 / 升级(升级终局披露需原文)
|
|
56
56
|
}
|
|
57
57
|
// 账目要求行(§6.31.3):仅会话态消化轮且未销账清单非空时注入(先投递后拼清单——
|
|
58
|
-
//
|
|
58
|
+
// 含本回合首投);两档同发(手动 / AUTO);transient(机器线独有)。
|
|
59
59
|
if (digestRound) {
|
|
60
60
|
const ids = pendingAsync.filter((e) => e._daDelivered === true).map((e) => e.id)
|
|
61
61
|
if (ids.length > 0) {
|
package/agent/setup.mjs
CHANGED
|
@@ -23,20 +23,10 @@ import { normalizeOrigin } from "../memory/origin.mjs"
|
|
|
23
23
|
import { searchOrigins } from "../memory/code-search.mjs"
|
|
24
24
|
import { INDEX_ORIGIN_ROW_WARN } from "../memory/schema.mjs"
|
|
25
25
|
import { resolveCompactThreshold } from "../config.mjs"
|
|
26
|
+
import { safeSliceUTF16 } from "../text-budget.mjs"
|
|
26
27
|
|
|
27
28
|
const DOC_SEARCH_LIMIT = 5
|
|
28
29
|
const DOC_CHUNK_PREVIEW_LEN = 300
|
|
29
|
-
/** UTF-16 安全截断(2026-09-02 deepseek 400 根因):slice(0, N) 按码元切会把 emoji 代理对切成孤立
|
|
30
|
-
* 高代理(如 🔴=U+D83D+DD34 只剩 D83D)——deepseek 解析器严格 UTF-16 报 400
|
|
31
|
-
* "unexpected end of hex escape"。截断点落在高代理上时向前收一个码元。 */
|
|
32
|
-
function safeSliceUTF16(text, max) {
|
|
33
|
-
if (text.length <= max) return text
|
|
34
|
-
const end = max
|
|
35
|
-
// 截断点恰在高代理(D800-DBFF)上 → 收到高代理之前(不带它)
|
|
36
|
-
const cp = text.charCodeAt(end - 1)
|
|
37
|
-
if (cp >= 0xd800 && cp <= 0xdbff) return text.slice(0, end - 1)
|
|
38
|
-
return text.slice(0, end)
|
|
39
|
-
}
|
|
40
30
|
const MEMORY_SEARCH_LIMIT = 3
|
|
41
31
|
|
|
42
32
|
/**
|
|
@@ -64,6 +54,11 @@ export async function prepareRun(agent, input, callbacks, {
|
|
|
64
54
|
|
|
65
55
|
agent._lastPromptTokens = null
|
|
66
56
|
agent._usageAtLen = null
|
|
57
|
+
// §6.17 估算自校准比(`_tok` / `_tokRatio`)**不在本处清**:本函数是每 run 的 pre-flight,
|
|
58
|
+
// 而在每轮用户消息上归零会让池化比退化成单样本(R2 失效)。初值在 `agent.mjs` 构造字面量,
|
|
59
|
+
// 清空点 = `/new`(`session-lifecycle.mjs`)∥ `key` 变自清(`token-window.mjs` `recordTokenSample`)。
|
|
60
|
+
// §6.17.1 链三自适应额度:run 级一次——本处复位(一次 = 全部额度;同一 run 内第二次独立越窗直接抛)。
|
|
61
|
+
agent._overflowRetried = false
|
|
67
62
|
// Cumulative provider cache accounting (batch 2026-10-18-context-cache-visibility):
|
|
68
63
|
// the service reports prompt_cache_hit/miss per response; the terminal faces each kept
|
|
69
64
|
// their own tally, so the CORE had none and `context` could not report cache efficiency.
|
package/agent/suspension.mjs
CHANGED
|
@@ -240,7 +240,7 @@ export function startSuspension(ctx) {
|
|
|
240
240
|
const done = (async () => {
|
|
241
241
|
carrier._suspended = true
|
|
242
242
|
// §6.31.2 会话标记(消化账务批 · 2026-10-05 · 台账 #930):挂起会话期 true——投递留容器 /
|
|
243
|
-
//
|
|
243
|
+
// 见账的唯一判据;退出(finally)置 false ⇒ 会话外回 fallback 语义。
|
|
244
244
|
carrier._daSession = true
|
|
245
245
|
hooks.onCounts?.(backgroundCounts(carrier))
|
|
246
246
|
let aborted = false
|
package/agent/turn-loop.mjs
CHANGED
|
@@ -11,8 +11,9 @@ import { specForModel, assistantToolCallMessage } from "../config.mjs"
|
|
|
11
11
|
import { executeToolCalls } from "./dispatch.mjs"
|
|
12
12
|
import { recordToolResults } from "./record-results.mjs"
|
|
13
13
|
import { injectPostTurn } from "./post-turn.mjs"
|
|
14
|
-
import { handleCompletion } from "./completion.mjs"
|
|
15
|
-
import { runCompactionCheck, injectTurnReminders, injectResponseReminders } from "./run-stages.mjs"
|
|
14
|
+
import { handleCompletion, isSubstantiveResponse } from "./completion.mjs"
|
|
15
|
+
import { runCompactionCheck, enforceContextFit, injectTurnReminders, injectResponseReminders } from "./run-stages.mjs"
|
|
16
|
+
import { recordTokenSample } from "../token-window.mjs"
|
|
16
17
|
import { callModelTurn } from "./chat-call.mjs"
|
|
17
18
|
import {
|
|
18
19
|
turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 TURN-CAP-CONTINUE.md §4)
|
|
@@ -48,6 +49,28 @@ export async function runTurnLoop(agent, {
|
|
|
48
49
|
// `context` 工具的 stats 报**同一口径**(不自行重算第二口径;VSC checkAndCompact 同款暂存)。
|
|
49
50
|
agent._ctxBasis = { threshold, overhead: compactionOverhead }
|
|
50
51
|
|
|
52
|
+
// §6.17.1 链三(越窗 400 自适应):单回合调用外包一层 catch——服务端说越窗 ⇒ 强制压一次 + 重试同一回合一次。
|
|
53
|
+
// 排除面:`stage ∈ {compress, distill}` 的请求**不经本函数**(压缩机与蒸馏各自直调 `chat`,续写子请求的
|
|
54
|
+
// 失败已在 `provider/core.mjs` 折进 `_warnings`)⇒ 本 catch 只接回合请求的 typed 越窗,不存在「已流出再重放」。
|
|
55
|
+
const callTurn = async (turnCtx) => {
|
|
56
|
+
try {
|
|
57
|
+
return await callModelTurn(agent, turnCtx)
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e?.kind !== "context_overflow") throw e
|
|
60
|
+
// 死循环守卫(批档 D6):额度 = run 级一次(`agent/setup.mjs` 每 run 复位)——已用过 ⇒ 原 typed 错误透传
|
|
61
|
+
// (三实数 + `/compact` 提示已在报文体里);同一 run 内第二次独立越窗不再自愈,用户 `/compact` 后可继续。
|
|
62
|
+
if (agent._overflowRetried) throw e
|
|
63
|
+
agent._overflowRetried = true
|
|
64
|
+
// 越窗 typed 错误有两个出口(同 kind,§6.17):HTTP 400 分类 ∥ 本地发送面净判——两面都归本链处理,
|
|
65
|
+
// 故估算不再作门(估算恰恰低估才有本链),整压一次;压后仍超 ⇒ 抛三实数、不再压。
|
|
66
|
+
await enforceContextFit(
|
|
67
|
+
agent, { threshold, callbacks, compactionOverhead, recentCallSigs, signal }, { forceCompact: true },
|
|
68
|
+
)
|
|
69
|
+
// 重试恰一次:此行不在 try 内——二次越窗的 typed 错误原样上抛(不截断、不静默)
|
|
70
|
+
return await callModelTurn(agent, turnCtx)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
51
74
|
// SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-UPSTREAM.md §6.27.4 消费点):子 → 父在飞消息的
|
|
52
75
|
// 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
|
|
53
76
|
// (同款先例 = `run-start.mjs` 的 `injectAsyncResult` 动态 import)。
|
|
@@ -127,10 +150,17 @@ export async function runTurnLoop(agent, {
|
|
|
127
150
|
// 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
|
|
128
151
|
await injectTurnReminders(agent, { depth })
|
|
129
152
|
|
|
130
|
-
const response = await
|
|
153
|
+
const response = await callTurn({
|
|
131
154
|
systemPrompt, toolSchemas, streamOutput, callbacks, signal, streamRuleFired, autoTurn, depth, turn,
|
|
132
155
|
})
|
|
133
156
|
|
|
157
|
+
// 空响应预算复位(AGENT-LOOP.md §6.5① · D-AL28):本轮响应**有实质** ⇒ 计数清 0(实质 = 有正文
|
|
158
|
+
// 或带工具调用;判据单源 = completion.mjs `isSubstantiveResponse`)。落位 = 本轮响应存在的第一处、
|
|
159
|
+
// 也是四支路(下方流规则 abort ∥ 用户中断 ∥ 工具批 ∥ 零工具)发散前的最后一处——落进任一分支都会漏其余三支。
|
|
160
|
+
// 与下方 `guardPushbacks = 0`(工具批执行处)分工:那处复位的是 verify ∥ advisor 推回预算、只看「有工具批」;
|
|
161
|
+
// 本处复位空响应预算、看「有实质」——正文本身已是实质,不必等工具批。
|
|
162
|
+
if (isSubstantiveResponse(response)) agent._emptyRetries = 0
|
|
163
|
+
|
|
134
164
|
// §6.31.5 落痕(消化账务批 · 2026-10-05 · 台账 #930 ∥ #939 收正):本 run 内**逐轮追加**——
|
|
135
165
|
// 每模型轮返回即写(会话态);首轮签收不为收口轮覆盖(见账取件读累积痕)。
|
|
136
166
|
// 落父字段(抗 `history` 整体替换——`context.mjs` `applyCompression`);非会话零动作。
|
|
@@ -194,6 +224,8 @@ export async function runTurnLoop(agent, {
|
|
|
194
224
|
agent._lastPromptTokens = response.usage.prompt_tokens
|
|
195
225
|
agent._usageAtLen = agent.history.length
|
|
196
226
|
}
|
|
227
|
+
// §6.17 估算自校准:服务端真值 ÷ 输入侧估算(`estimated` = 发送面净判同一个 estPrompt)
|
|
228
|
+
recordTokenSample(agent, agent.provider, response.estimated, response.usage)
|
|
197
229
|
}
|
|
198
230
|
|
|
199
231
|
// Warn on abnormal finish reasons — the response may be incomplete/truncated.
|