thincoder 0.12.58 → 0.12.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -2
- package/README.md +3 -3
- package/bin/thincoder.mjs +88 -19
- package/package.json +4 -3
- package/src/acp/bridge.mjs +135 -26
- package/src/advisor/messages.mjs +57 -4
- package/src/advisor/run.mjs +119 -79
- package/src/advisor.mjs +34 -7
- package/src/agent/completion.mjs +17 -11
- package/src/agent/dispatch.mjs +182 -22
- package/src/agent/helpers.mjs +71 -4
- package/src/agent/record-results.mjs +46 -10
- package/src/agent/run-stages.mjs +227 -0
- package/src/agent/setup-reminders.mjs +62 -0
- package/src/agent/setup.mjs +107 -20
- package/src/agent/spawn-child.mjs +54 -4
- package/src/agent-tools/advisor-async.mjs +456 -0
- package/src/agent-tools/advisor.mjs +133 -109
- package/src/agent-tools/async-settle.mjs +191 -0
- package/src/agent-tools/consult.mjs +154 -104
- package/src/agent-tools/design-token.mjs +104 -0
- package/src/agent-tools/eng.mjs +26 -30
- package/src/agent-tools/escalate-async.mjs +286 -0
- package/src/agent-tools/goal.mjs +11 -1
- package/src/agent-tools/read-history.mjs +284 -0
- package/src/agent-tools/recent-changes.mjs +2 -1
- package/src/agent-tools/settings.mjs +152 -0
- package/src/agent-tools/skill.mjs +2 -1
- package/src/agent-tools/subagent-actions.mjs +470 -0
- package/src/agent-tools/subagent-async.mjs +382 -0
- package/src/agent-tools/subagent-panel.mjs +153 -0
- package/src/agent-tools/subagent-run.mjs +202 -0
- package/src/agent-tools/subagent-scheduler.mjs +343 -0
- package/src/agent-tools/subagent-spawn.mjs +406 -0
- package/src/agent-tools/subagent.mjs +203 -377
- package/src/agent-tools/task.mjs +4 -3
- package/src/agent-tools/timer.mjs +9 -4
- package/src/agent-tools/verify.mjs +198 -238
- package/src/agent-tools.mjs +1 -0
- package/src/agent.mjs +145 -242
- package/src/auto-think.mjs +14 -0
- package/src/cli/distill-command.mjs +10 -4
- package/src/cli/make-agent.mjs +4 -1
- package/src/cli/memory-command.mjs +2 -1
- package/src/cli/permission.mjs +8 -1
- package/src/cli/setup-wizard.mjs +17 -12
- package/src/config.mjs +61 -8
- package/src/context.mjs +81 -163
- package/src/crash-reports.mjs +123 -0
- package/src/distill.mjs +30 -12
- package/src/escape.mjs +6 -4
- package/src/explore-distill.mjs +155 -0
- package/src/log.mjs +195 -0
- package/src/memory/code-sync.mjs +2 -1
- package/src/memory/core.mjs +11 -72
- package/src/memory/delete.mjs +234 -0
- package/src/memory/docs.mjs +206 -87
- package/src/memory.mjs +3 -1
- package/src/model-specs.mjs +15 -1
- package/src/peer-domains.mjs +265 -0
- package/src/peer-instances.mjs +231 -0
- package/src/prompt-overlays.mjs +25 -0
- package/src/prompts/advisor-design.md +18 -39
- package/src/prompts/advisor-round1.md +20 -32
- package/src/prompts/advisor-round2.md +16 -16
- package/src/prompts/advisor-round3.md +16 -16
- package/src/prompts/coder.md +7 -28
- package/src/prompts/consult-base.md +4 -11
- package/src/prompts/discipline.md +31 -44
- package/src/prompts/eng-coder.md +9 -34
- package/src/prompts/engineering-sub.md +10 -8
- package/src/prompts/engineering.md +61 -264
- package/src/prompts/explore.md +4 -14
- package/src/prompts/main.md +18 -35
- package/src/prompts/methodology-template.md +32 -38
- package/src/prompts/plan.md +2 -9
- package/src/prompts/system.md +18 -35
- package/src/provider/core.mjs +62 -69
- package/src/provider/errors.mjs +76 -0
- package/src/provider/retry.mjs +8 -45
- package/src/session-gc.mjs +214 -0
- package/src/session-guard.mjs +47 -0
- package/src/session-rename.mjs +38 -0
- package/src/session-slots.mjs +181 -58
- package/src/session.mjs +48 -89
- package/src/token-ttl.mjs +273 -0
- package/src/tools/apply_patch.md +3 -1
- package/src/tools/bash.md +1 -1
- package/src/tools/checklist-sync.mjs +181 -0
- package/src/tools/checklist.mjs +52 -39
- package/src/tools/delete.md +1 -0
- package/src/tools/edit-batch.mjs +131 -44
- package/src/tools/edit-diff.mjs +348 -0
- package/src/tools/edit.md +20 -13
- package/src/tools/execute.md +7 -7
- package/src/tools/execute.mjs +55 -24
- package/src/tools/file.mjs +25 -70
- package/src/tools/file_ops.md +2 -1
- package/src/tools/get_current_time.md +3 -1
- package/src/tools/git.mjs +14 -6
- package/src/tools/glob-dialect.mjs +130 -0
- package/src/tools/glob.md +3 -3
- package/src/tools/grep.md +1 -1
- package/src/tools/hashline_edit.md +2 -0
- package/src/tools/index.mjs +3 -3
- package/src/tools/insert_after.md +2 -1
- package/src/tools/lint.md +2 -0
- package/src/tools/lsp.md +4 -1
- package/src/tools/ops.mjs +175 -3
- package/src/tools/patch.mjs +84 -13
- package/src/tools/question.md +5 -1
- package/src/tools/repomap.mjs +1 -1
- package/src/tools/shared.mjs +18 -25
- package/src/tools/system.mjs +50 -30
- package/src/tools/tree.md +2 -1
- package/src/tools/wait_for.md +22 -0
- package/src/tools/web.mjs +5 -3
- package/src/tools/websearch.md +2 -1
- package/src/tools/write.md +2 -0
- package/src/traces/trace-store.mjs +224 -0
- package/src/tui/agent-turn.mjs +179 -27
- package/src/tui/clipboard.mjs +15 -4
- package/src/tui/cmd-config.mjs +77 -16
- package/src/tui/cmd-eng.mjs +20 -16
- package/src/tui/cmd-extract.mjs +1 -1
- package/src/tui/cmd-mcp.mjs +17 -2
- package/src/tui/cmd-new.mjs +3 -2
- package/src/tui/cmd-session.mjs +19 -4
- package/src/tui/cmd-think.mjs +11 -11
- package/src/tui/cmd-upgrade.mjs +19 -4
- package/src/tui/config-helpers.mjs +28 -16
- package/src/tui/distill-cmd.mjs +1 -1
- package/src/tui/index.mjs +31 -96
- package/src/tui/interaction.mjs +13 -2
- package/src/tui/key-handler.mjs +105 -155
- package/src/tui/key-modes.mjs +215 -0
- package/src/tui/layout.mjs +22 -1
- package/src/tui/mouse.mjs +46 -0
- package/src/tui/pickers.mjs +51 -25
- package/src/tui/render-conversation.mjs +13 -161
- package/src/tui/render-frame.mjs +27 -10
- package/src/tui/render-loop.mjs +4 -1
- package/src/tui/render-segments.mjs +182 -0
- package/src/tui/startup.mjs +40 -0
- package/src/tui/subagent-blocks.mjs +272 -262
- package/src/tui/subagent-children.mjs +176 -0
- package/src/tui/subagent-freeze.mjs +172 -0
- package/src/tui/subagent-panel.mjs +125 -12
- package/src/tui/suspension-drive.mjs +351 -0
- package/src/tui/tool-args.mjs +10 -2
- package/src/tui/tool-display.mjs +142 -0
- package/src/tui/tool-events.mjs +127 -231
- package/src/tui/tui-lifecycle.mjs +29 -0
- package/src/tui/update-notice.mjs +76 -0
- package/src/tui/wizard.mjs +48 -12
- package/src/agent-tools/escalate.mjs +0 -179
- package/src/agent-tools/subagent-check.mjs +0 -107
- package/src/tools/exec-prelude.mjs +0 -84
package/src/cli/make-agent.mjs
CHANGED
|
@@ -3,9 +3,12 @@ import { join } from "node:path"
|
|
|
3
3
|
import { createAgent } from "../agent.mjs"
|
|
4
4
|
import { loadConfig, configDir } from "../config.mjs"
|
|
5
5
|
import { createMemory, memoryTools, syncDir, codeSearchTool, docSearchTool } from "../memory.mjs"
|
|
6
|
+
import { settingsTool } from "../agent-tools/settings.mjs"
|
|
6
7
|
import { repoOutlineTool } from "../tools/repomap.mjs"
|
|
7
8
|
import { builtinTools } from "../tools/index.mjs"
|
|
8
9
|
import { discoverRules } from "../rules.mjs"
|
|
10
|
+
// R10 L2(MULTI-INSTANCE-COLLAB §2a.4 D-L2b):peer_instances 只读工具——挂感知模块导出
|
|
11
|
+
import { peerInstancesTool } from "../peer-instances.mjs"
|
|
9
12
|
|
|
10
13
|
/** Assemble an agent with memory, MCP tools, and code/doc indices attached (sync all layers, then return) */
|
|
11
14
|
export async function assembleAgent() {
|
|
@@ -48,7 +51,7 @@ export async function assembleAgent() {
|
|
|
48
51
|
await ensureClone(team)
|
|
49
52
|
await syncDir(memory, { layer: "team", dir: team.dir })
|
|
50
53
|
}
|
|
51
|
-
const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd)]
|
|
54
|
+
const baseTools = [...builtinTools, ...memoryTools(memory, { cwd, projectDir: config.memory.projectDir, author: gitAuthor(), team }), codeSearchTool(memory), docSearchTool(memory), repoOutlineTool(memory.db, cwd), settingsTool(), peerInstancesTool]
|
|
52
55
|
|
|
53
56
|
// MCP servers: connect in parallel (a dead server won't block startup), collect failures as warnings (stderr invisible in TUI, passed via agent object)
|
|
54
57
|
const mcpServers = config.mcp?.servers ?? []
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { join } from "node:path"
|
|
2
2
|
import { loadConfig } from "../config.mjs"
|
|
3
3
|
import { teamConfig } from "./make-agent.mjs"
|
|
4
|
-
import { put, search, list
|
|
4
|
+
import { put, search, list } from "../memory/core.mjs"
|
|
5
|
+
import { deleteByUid } from "../memory/delete.mjs"
|
|
5
6
|
|
|
6
7
|
/** thincoder memory <list|search|put|remove> subcommands.
|
|
7
8
|
* opts.dirs: { project, team } layer directories for project/team file deletion (tests inject their own);
|
package/src/cli/permission.mjs
CHANGED
|
@@ -19,7 +19,14 @@ export function formatPermission(name, args) {
|
|
|
19
19
|
}
|
|
20
20
|
if (base === "delete") return `${args.path}${args.force ? "(force:跟踪文件也删)" : ""}`
|
|
21
21
|
if (base === "subagent") return cap(args.task ?? "", 500)
|
|
22
|
-
if (base === "
|
|
22
|
+
if (base === "memory") {
|
|
23
|
+
// §6 action-routed preview: put shows content, batch delete/clear show the gate args
|
|
24
|
+
const action = String(args.action ?? "")
|
|
25
|
+
if (action === "put") return `[${args.type ?? ""}] ${args.title ?? ""}\n${cap(args.content ?? "", 500)}`
|
|
26
|
+
if (action === "delete") return args.id ? `id=${args.id}${args.layer ? ` layer=${args.layer}` : ""}` : `batch delete layer=${args.layer ?? ""} type=${args.type ?? ""} keyword=${args.keyword ?? ""} confirm=${args.confirm}`
|
|
27
|
+
if (action === "clear") return `clear layer=${args.layer ?? ""} confirm=${args.confirm}`
|
|
28
|
+
return cap(summarize(args), 300)
|
|
29
|
+
}
|
|
23
30
|
return cap(summarize(args), 300)
|
|
24
31
|
}
|
|
25
32
|
|
package/src/cli/setup-wizard.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
1
|
import { createInterface } from "node:readline"
|
|
3
|
-
import { configPath,
|
|
2
|
+
import { configPath, writeConfigAtomic, PROVIDER_PRESETS } from "../config.mjs"
|
|
4
3
|
|
|
5
4
|
/** First-time setup (TTY chat / distill): ask a few questions to configure a provider, save to disk, return runtime provider. Cancel returns null. */
|
|
6
5
|
export async function setupWizard() {
|
|
@@ -52,16 +51,22 @@ export async function setupWizard() {
|
|
|
52
51
|
return null
|
|
53
52
|
}
|
|
54
53
|
const embedKey = (await ask("Optional: embedding API key (SiliconFlow, for vector search; press Enter to skip): ")).trim()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
// D-F5b:磁盘新鲜读 → mutate → mtime 门控写(writeConfigAtomic 收口);冲突 = 放弃
|
|
55
|
+
// + 提示重试(首配场景另有实例同时写盘——极低概率;不自动合并——决策点① A)
|
|
56
|
+
const r = writeConfigAtomic(configPath, (raw) => {
|
|
57
|
+
const providers = raw.providers?.length ? raw.providers : []
|
|
58
|
+
const existing = providers.find((p) => p.name === name)
|
|
59
|
+
if (existing) Object.assign(existing, { baseURL, model, apiKey })
|
|
60
|
+
else providers.push({ name, baseURL, model, apiKey })
|
|
61
|
+
raw.providers = providers
|
|
62
|
+
raw.activeProvider = name
|
|
63
|
+
delete raw.activeModel // reset to default model
|
|
64
|
+
if (embedKey) raw.embedding = { ...(raw.embedding ?? {}), apiKey: embedKey }
|
|
65
|
+
})
|
|
66
|
+
if (!r.ok) {
|
|
67
|
+
console.error("config changed on disk concurrently — retry")
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
65
70
|
console.error(`Configured: ${name} / ${model} (saved to ${configPath})`)
|
|
66
71
|
console.error(embedKey ? "Vector search enabled\n" : "(No embedding key configured: memory search will use text-only FTS. Add embedding.apiKey to config.json to enable vector search later.)\n")
|
|
67
72
|
return { name, baseURL, model, apiKey }
|
package/src/config.mjs
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
* API key can fall back to environment variables (when not configured in providers).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
8
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
9
9
|
import { homedir } from "node:os"
|
|
10
|
-
import { join } from "node:path"
|
|
10
|
+
import { dirname, join } from "node:path"
|
|
11
11
|
|
|
12
12
|
export const configDir = join(homedir(), ".thincoder")
|
|
13
13
|
export const configPath = join(configDir, "config.json")
|
|
@@ -55,6 +55,14 @@ export const DEFAULTS = {
|
|
|
55
55
|
advisor: { guard: false }, // code review is always available; guard: true pushes completion back until reviewed (opt-in). Also accepts provider/model/thinking/reasoningEffort/timeoutMs overrides. Deprecated: enabled (2026-08-21)
|
|
56
56
|
autoThink: false, // auto-classify task difficulty and set reasoning effort per-turn
|
|
57
57
|
engineering: false, // strict methodology enforcement — read METHODOLOGY.md, design-before-code
|
|
58
|
+
// Async subagent pool limits per role domain (AGENT-LOOP.md §24 D-24a/R14):
|
|
59
|
+
// { engCoder, other } — eng-coder pool / other-role pool, defaults 4/4 (user
|
|
60
|
+
// ruling "eng-coder 四路,其他 4 路"). Runtime-validated at every pool admission
|
|
61
|
+
// (positive integer ≥1, invalid/absent keys fall back to 4 — settings tool and
|
|
62
|
+
// the /config 并发池 menu write this key; change applies to the next spawn).
|
|
63
|
+
// ⚠ 与 subagent-async.mjs ASYNC_POOL_LIMITS 逐键同值(运行时回退常量)——耦合锚
|
|
64
|
+
// T-24a4 断言锁住——勿单侧改默认。
|
|
65
|
+
poolLimits: { engCoder: 4, other: 4 },
|
|
58
66
|
},
|
|
59
67
|
memory: {
|
|
60
68
|
dbPath: join(configDir, "memory.db"),
|
|
@@ -73,6 +81,10 @@ export const DEFAULTS = {
|
|
|
73
81
|
provider: "tavily", // structured search API; empty apiKey → fall back to Bing HTML scraping
|
|
74
82
|
apiKey: "", // Tavily key (tvly-...) — optional
|
|
75
83
|
},
|
|
84
|
+
traces: {
|
|
85
|
+
enabled: false, // §18.6 D-TR6 修订(2026-09-05 用户裁定——发布隐私:"不希望用户那边也采集"):轨迹存档默认 OFF——新用户零采集;本地调试分析可显式开(~/.thincoder/config.json traces.enabled:true)
|
|
86
|
+
retentionHours: 24, // D-TR10:轨迹文件保留小时数——CLI 启动时删除超过该时长的文件(默认 24h)
|
|
87
|
+
},
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
// Model capability table + spec lookup live in model-specs.mjs (2026-08-31
|
|
@@ -195,6 +207,7 @@ export function loadConfig() {
|
|
|
195
207
|
agent: { ...DEFAULTS.agent, ...config.agent },
|
|
196
208
|
memory: { ...DEFAULTS.memory, ...config.memory },
|
|
197
209
|
embedding: { ...DEFAULTS.embedding, ...config.embedding },
|
|
210
|
+
traces: { ...DEFAULTS.traces, ...config.traces },
|
|
198
211
|
}
|
|
199
212
|
|
|
200
213
|
// providers[].context (K units, PROVIDER.md §15 D-C1): positive integer only — invalid
|
|
@@ -363,12 +376,52 @@ function mcpFingerprint(s) {
|
|
|
363
376
|
}
|
|
364
377
|
|
|
365
378
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
379
|
+
* R10 F5(D-F5b,2026-09-06)——config.json 写前 mtime 门控收口函数(session-rename
|
|
380
|
+
* mtime-conflict 先例同型——MULTI-INSTANCE-COLLAB.md §2a.2)。所有 config.json 写点
|
|
381
|
+
* (config-helpers persistRaw / cmd-config saveProxy / cli setup-wizard / settings
|
|
382
|
+
* writeDisk)都经它落盘。
|
|
383
|
+
*
|
|
384
|
+
* 本函数持有整条「新鲜读 → mutate 单操作 → 写前重 stat → 写」链:
|
|
385
|
+
* - t0 = 写前重 stat 的比对基线,取在**新鲜读之前**(stat→read 序):若对端在本端
|
|
386
|
+
* stat 与 read 之间的微窗口写入,只会造成假冲突(放弃重试),绝不会带着旧内容覆盖
|
|
387
|
+
* 对端新值——read→stat 序存在漏检窗口(stat 已反映对端新 mtime → 门控放行旧内容)。
|
|
388
|
+
* - 写前重 stat ≠ t0 → **放弃**本次写(D-F5a 后各流已是 fresh 单操作语义,磁盘上对端
|
|
389
|
+
* 的新值保持在线不抹);先 copy `.bak-{ts}` 留现场(仅冲突时——config 低频写不膨胀;
|
|
390
|
+
* copy 而非 rename:冲突即放弃、本体不动,"保现场"是额外副本,非轮转腾位)。
|
|
391
|
+
* - 返回 { ok:false, reason:"mtime-conflict" },调用方提示 "config changed on disk
|
|
392
|
+
* concurrently — retry"——不自动合并(config 是用户显式操作——重试比猜测合并安全,
|
|
393
|
+
* 决策点① A)。
|
|
394
|
+
* - 文件缺失(首写)→ t0 = null;对端在本端读后创建 → null ≠ 新 mtime → 冲突放弃。
|
|
395
|
+
* - 畸形文件拒写(throw,绝不静默覆盖);写后 chmod 0600 尽力而为(saveConfig 旧语义)。
|
|
396
|
+
*
|
|
397
|
+
* @param path config.json 路径(生产默认 configPath;测试注入 tmp 路径)
|
|
398
|
+
* @param mutate 在磁盘新鲜 raw 上执行单操作的同步回调(如 push/splice/单字段补丁)
|
|
399
|
+
* @returns { ok: true } | { ok: false, reason: "mtime-conflict" }
|
|
368
400
|
*/
|
|
369
|
-
export function
|
|
370
|
-
|
|
401
|
+
export function writeConfigAtomic(path, mutate) {
|
|
402
|
+
const mtimeOf = (p) => {
|
|
403
|
+
try { return statSync(p).mtimeMs } catch { return null } // 缺失 → null(t0 比对基线)
|
|
404
|
+
}
|
|
405
|
+
const t0 = mtimeOf(path) // stat 先于 read(安全方向——见头注释)
|
|
406
|
+
const text = existsSync(path) ? readFileSync(path, "utf8") : null
|
|
407
|
+
let raw = {}
|
|
408
|
+
if (text !== null) {
|
|
409
|
+
try {
|
|
410
|
+
raw = JSON.parse(text)
|
|
411
|
+
} catch (error) {
|
|
412
|
+
throw new Error(`config file not parseable — refusing to overwrite: ${path} — ${error.message}`, { cause: error })
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
mutate(raw)
|
|
416
|
+
const t1 = mtimeOf(path)
|
|
417
|
+
if (t0 !== t1) {
|
|
418
|
+
// 对端在我们新鲜读后改过磁盘 → 放弃本次写(对端内容保持在线);.bak 副本留现场
|
|
419
|
+
try { if (existsSync(path)) copyFileSync(path, `${path}.bak-${Date.now()}`) } catch { /* 现场保留失败不阻断冲突报告 */ }
|
|
420
|
+
return { ok: false, reason: "mtime-conflict" }
|
|
421
|
+
}
|
|
422
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
371
423
|
// 0600: config.json contains API keys, must not be world-readable (POSIX; chmod is best-effort on Windows)
|
|
372
|
-
writeFileSync(
|
|
373
|
-
try { chmodSync(
|
|
424
|
+
writeFileSync(path, JSON.stringify(raw, null, 2) + "\n", { encoding: "utf8", mode: 0o600 })
|
|
425
|
+
try { chmodSync(path, 0o600) } catch { /* may fail on Windows, ignore */ }
|
|
426
|
+
return { ok: true }
|
|
374
427
|
}
|
package/src/context.mjs
CHANGED
|
@@ -37,9 +37,13 @@ export function estimateTokens(messages) {
|
|
|
37
37
|
const KEEP_HEAD = 0 // No dedicated head: earliest messages may be a COMPLETED earlier task in multi-task
|
|
38
38
|
// sessions — keeping them verbatim anchored attention on stale work. Everything before the tail is
|
|
39
39
|
// summarized (the summary itself distinguishes completed vs in-progress work; see SUMMARIZE_PROMPT).
|
|
40
|
-
// Tail
|
|
41
|
-
//
|
|
42
|
-
//
|
|
40
|
+
// Tail count formula (D4): window-adaptive (~30 msgs per 100K — old fixed 10 too thin on 1M), capped
|
|
41
|
+
// at 40% of history; §9 D-T1/D-T2 make the count only a CANDIDATE — a token budget (TAIL_BUDGET_FRACTION
|
|
42
|
+
// × window − SUMMARY_TOKEN_ESTIMATE ≈1K, §8) tightens it over pair-safe boundaries when compaction runs,
|
|
43
|
+
// never below TAIL_FLOOR_MESSAGES; ordinary sessions never reach it (D-T4: trigger 0.6 untouched).
|
|
44
|
+
const TAIL_BUDGET_FRACTION = 0.15
|
|
45
|
+
const SUMMARY_TOKEN_ESTIMATE = 1000 // §8: summary output target ~1K tokens — reserved from the 15%
|
|
46
|
+
const TAIL_FLOOR_MESSAGES = 10 // §9 D-T2: the tail keeps ≥10 verbatim messages — floor beats budget
|
|
43
47
|
function keepTailSize(provider, historyLen) {
|
|
44
48
|
// provider is guaranteed at every call site (runAgent always builds one); providerSpec
|
|
45
49
|
// degrades to DEFAULT_SPEC (128K) only if provider is somehow absent — acceptable
|
|
@@ -48,6 +52,10 @@ function keepTailSize(provider, historyLen) {
|
|
|
48
52
|
const ctxWindow = providerSpec(provider).context
|
|
49
53
|
return Math.min(Math.max(10, Math.floor((ctxWindow / 100_000) * 30)), Math.floor(historyLen * 0.4))
|
|
50
54
|
}
|
|
55
|
+
// §9 D-T1 tail token budget: window×15% − summary ~1K — the compressed history segment (summary + placeholder + tail) lands ≈ 15% (B 口径 §9.5).
|
|
56
|
+
function tailBudgetTokens(provider) {
|
|
57
|
+
return Math.max(0, Math.floor(providerSpec(provider).context * TAIL_BUDGET_FRACTION) - SUMMARY_TOKEN_ESTIMATE)
|
|
58
|
+
}
|
|
51
59
|
|
|
52
60
|
export const SUMMARIZE_PROMPT = `You are a conversation compressor. Summarize the following agent work log into a compact summary for use as context in the ongoing conversation.
|
|
53
61
|
Requirements:
|
|
@@ -59,7 +67,7 @@ Requirements:
|
|
|
59
67
|
- Explicitly list UNRESOLVED ISSUES / TODOs: anything still open plus the next steps — so post-compaction recovery knows where to resume
|
|
60
68
|
- Drop: pleasantries, repetition, fine-grained tool output details
|
|
61
69
|
- Honestly mark uncertain items: anything not actually verified must say "unverified"; do not present guesses as facts
|
|
62
|
-
- Use bullet-point output
|
|
70
|
+
- Use bullet-point output. Stay under ~1K tokens (≈1000 Chinese chars / 4000 ASCII chars) — a hard target. An oversized summary wastes window and dilutes the tail; the old unbounded-length guidance is deprecated. When over budget, trim in this order: completed recaps to one line; FILES CHANGED why-notes to bare paths; in-progress prose tightened. NEVER cut design anchors or UNRESOLVED ISSUES/TODOs — recovery depends on them.
|
|
63
71
|
|
|
64
72
|
Work log:
|
|
65
73
|
`
|
|
@@ -68,7 +76,7 @@ Work log:
|
|
|
68
76
|
const COMPACTION_PREFIX =
|
|
69
77
|
"[Context was automatically compacted. Below is a summary of earlier work. " +
|
|
70
78
|
"Treat it as notes, not proof — trust its conclusions (don't redo what it reports as done) " +
|
|
71
|
-
"but re-verify transient state with tools. Check
|
|
79
|
+
"but re-verify transient state with tools. Check memory search for any missing decisions.]\n\n"
|
|
72
80
|
|
|
73
81
|
/** After this many consecutive compaction summary failures, degrade to deterministic truncation (losing info is better than task-killing 400 errors) */
|
|
74
82
|
export const COMPRESS_FAILURE_LIMIT = 3
|
|
@@ -84,12 +92,13 @@ const FALLBACK_NOTE =
|
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
94
|
* Split history into head / middle (to be summarized) / tail; return null if no middle to compress.
|
|
87
|
-
* head is normally empty (KEEP_HEAD = 0 — earliest messages go into the summary); the
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* the
|
|
95
|
+
* head is normally empty (KEEP_HEAD = 0 — earliest messages go into the summary); the tool_calls-extension logic below is defensive for future KEEP_HEAD > 0.
|
|
96
|
+
* The tail boundary must include any assistant whose tool results are in the tail — if the assistant is in the middle, the summary swallows it, leaving orphan tool results → protocol 400.
|
|
97
|
+
* `budgetTokens` (optional, §9 D-T1): when the candidate's estimate exceeds it, the boundary moves
|
|
98
|
+
* forward until the tail fits — never below the D-T2 floor (10 msgs, or the candidate itself when
|
|
99
|
+
* the 40% cap made it < 10 — short history).
|
|
91
100
|
*/
|
|
92
|
-
function splitHistory(history, keepTail) {
|
|
101
|
+
function splitHistory(history, keepTail, budgetTokens = null) {
|
|
93
102
|
if (history.length <= KEEP_HEAD + keepTail + 1) return null
|
|
94
103
|
let headEnd = KEEP_HEAD
|
|
95
104
|
// head must not end with dangling tool_calls: when assistant declares tool_calls, all its tool results must stay in head.
|
|
@@ -97,10 +106,24 @@ function splitHistory(history, keepTail) {
|
|
|
97
106
|
if (history[headEnd - 1]?.role === "assistant" && history[headEnd - 1].tool_calls?.length) {
|
|
98
107
|
while (headEnd < history.length && history[headEnd].role === "tool") headEnd++
|
|
99
108
|
}
|
|
100
|
-
|
|
109
|
+
const candidate = repairedTailStart(history, headEnd, history.length - keepTail)
|
|
110
|
+
if (candidate <= headEnd) return null
|
|
111
|
+
let tailStart = candidate
|
|
112
|
+
// §9 D-T1: tighten only above the floor — a candidate ≤ 10 IS the floor (short history under the 40% cap must not tighten further, review #5); the floor is D5-repaired too.
|
|
113
|
+
if (budgetTokens > 0 && keepTail > TAIL_FLOOR_MESSAGES) {
|
|
114
|
+
const floor = repairedTailStart(history, headEnd, history.length - TAIL_FLOOR_MESSAGES)
|
|
115
|
+
if (floor > candidate) tailStart = tightenTailByBudget(history, candidate, floor, budgetTokens)
|
|
116
|
+
}
|
|
117
|
+
return { headEnd, tailStart }
|
|
118
|
+
}
|
|
101
119
|
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
/**
|
|
121
|
+
* D5 tail-side pairing repair for a raw cut at history.length − tailCount: pull into the tail any
|
|
122
|
+
* assistant whose tool results are in the tail (the summary swallowing the owner leaves orphan tool
|
|
123
|
+
* results → protocol 400), then skip orphan tool messages at the new boundary. Single-assistant
|
|
124
|
+
* assumption (nearest owner only — a tail spans at most one assistant→tools cycle); bounds-guarded.
|
|
125
|
+
*/
|
|
126
|
+
function repairedTailStart(history, headEnd, tailStart) {
|
|
104
127
|
const tailToolIds = new Set()
|
|
105
128
|
for (let i = tailStart; i < history.length; i++) {
|
|
106
129
|
if (history[i].role === "tool") tailToolIds.add(history[i].tool_call_id)
|
|
@@ -112,16 +135,28 @@ function splitHistory(history, keepTail) {
|
|
|
112
135
|
break
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
// skip orphan tool messages at the new tail boundary (tool whose assistant was pulled in above)
|
|
117
|
-
// NOTE: single-assistant assumption — the backwards scan pulls the nearest owner only; in
|
|
118
|
-
// practice a tail spans at most one assistant→tools cycle (parallel calls share one assistant).
|
|
119
|
-
// Bounds-guarded so an all-tool tail cannot push tailStart past history.length.
|
|
120
138
|
while (tailStart < history.length && tailStart > headEnd && history[tailStart].role === "tool") {
|
|
121
139
|
tailStart++
|
|
122
140
|
}
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
return tailStart
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* §9 D-T1 budget tightening (pair-safe, review #2): walk the boundary FORWARD (fewer tail messages —
|
|
146
|
+
* the rest joins the summary) while the tail's estimated tokens exceed the budget. Only pair-safe
|
|
147
|
+
* positions may stop the walk: a boundary ON a tool message would orphan its owner assistant into the
|
|
148
|
+
* middle (D5); pairing is contiguous in the machine line (§6 note) — every non-tool boundary is safe.
|
|
149
|
+
* No fit before the floor → keep the floor, accept the overrun.
|
|
150
|
+
*/
|
|
151
|
+
function tightenTailByBudget(history, start, floorStart, budgetTokens) {
|
|
152
|
+
const suffixTokens = new Array(history.length + 1)
|
|
153
|
+
suffixTokens[history.length] = 0
|
|
154
|
+
for (let i = history.length - 1; i >= 0; i--) suffixTokens[i] = suffixTokens[i + 1] + estimateTokens([history[i]])
|
|
155
|
+
if (suffixTokens[start] <= budgetTokens) return start // already fits — ordinary sessions stay untouched (D-T2)
|
|
156
|
+
for (let p = start + 1; p <= floorStart; p++) { // first fit keeps the most recent verbatim context
|
|
157
|
+
if (history[p].role !== "tool" && suffixTokens[p] <= budgetTokens) return p
|
|
158
|
+
}
|
|
159
|
+
return floorStart
|
|
125
160
|
}
|
|
126
161
|
|
|
127
162
|
/**
|
|
@@ -132,9 +167,14 @@ function splitHistory(history, keepTail) {
|
|
|
132
167
|
* Machine-only messages ([System reminder:...], compaction notes, task/plan/checkpoint re-injections)
|
|
133
168
|
* are pushed directly to agent.history WITHOUT going through here, so they never enter _fullHistory.
|
|
134
169
|
* The two lines are written independently at the source — no after-the-fact delta sync.
|
|
170
|
+
* Message timestamps (SESSION.md §9 D-S1): stamped HERE once at push time (epoch ms) — a single
|
|
171
|
+
* point covers every real message. Pre-existing ts (e.g. from another end writing the shared slot)
|
|
172
|
+
* is preserved; restored old messages keep no ts rather than getting a misleading backdate (D-S3).
|
|
173
|
+
* ts is a LOCAL-ONLY field — the send layer strips it before any provider request (T-S3).
|
|
135
174
|
*/
|
|
136
175
|
export function pushReal(agent, msg) {
|
|
137
176
|
if (!Array.isArray(agent._fullHistory)) agent._fullHistory = []
|
|
177
|
+
if (msg && msg.ts === undefined) msg.ts = Date.now()
|
|
138
178
|
agent._fullHistory.push(msg)
|
|
139
179
|
agent.history.push(msg)
|
|
140
180
|
}
|
|
@@ -148,10 +188,14 @@ function applyCompression(agent, headEnd, tailStart, note) {
|
|
|
148
188
|
// possibly-completed earlier requests.
|
|
149
189
|
const head = agent.history.slice(0, headEnd)
|
|
150
190
|
const tail = agent.history.slice(tailStart)
|
|
191
|
+
// SESSION.md §9 D-S1: compaction-injected messages (note + "Understood") carry a ts —
|
|
192
|
+
// Date.now() at the compaction moment. They are machine-only (never in _fullHistory),
|
|
193
|
+
// but the machine-line timeline stays consistent for any audit use.
|
|
194
|
+
const now = Date.now()
|
|
151
195
|
agent.history = [
|
|
152
196
|
...head,
|
|
153
|
-
{ role: "user", content: note },
|
|
154
|
-
{ role: "assistant", content: "Understood. I'll continue from these notes, re-verifying anything transient." },
|
|
197
|
+
{ role: "user", content: note, ts: now },
|
|
198
|
+
{ role: "assistant", content: "Understood. I'll continue from these notes, re-verifying anything transient.", ts: now },
|
|
155
199
|
...tail,
|
|
156
200
|
]
|
|
157
201
|
// Compaction REBUILDS the machine line (head + note + "Understood" + tail), so the pre-compaction
|
|
@@ -217,7 +261,7 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
217
261
|
if (tokens <= threshold) return false
|
|
218
262
|
|
|
219
263
|
const keepTail = keepTailSize(agent.provider, history.length)
|
|
220
|
-
const split = splitHistory(history, keepTail)
|
|
264
|
+
const split = splitHistory(history, keepTail, tailBudgetTokens(agent.provider))
|
|
221
265
|
if (!split) {
|
|
222
266
|
// History is too short (≤KEEP_HEAD+keepTail+1 messages) to find a middle section, but tokens exceed threshold — typically a single giant message
|
|
223
267
|
// (large paste / huge injection). When summarization has no room, degrade to deterministic shrinking to ensure context always reduces
|
|
@@ -243,13 +287,21 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
243
287
|
// Silent by design (D11): no onToken/onReasoning — the compaction process must not stream to the frontend.
|
|
244
288
|
// signal propagates user cancellation (Ctrl+C) to the in-flight summary call.
|
|
245
289
|
// Compression visibility (CONTEXT-COMPACTION.md §7 D-C1/D-C2): the frontend learns the compression
|
|
246
|
-
// STARTED right before the LLM call ("Compressing context… / summarizing N messages" panel) — only
|
|
290
|
+
// STARTED right before the summary LLM call ("Compressing context… / summarizing N messages" panel) — only
|
|
247
291
|
// the lifecycle is surfaced, never the summary body. N = the number of history messages being summarized.
|
|
248
292
|
callbacks?.onCompressStart?.({ messages: middle.length })
|
|
249
293
|
const startedAt = performance.now()
|
|
250
294
|
const summary = await chat({ ...agent.provider, thinking: null, reasoningEffort: null }, {
|
|
251
295
|
messages: [{ role: "user", content: SUMMARIZE_PROMPT + serialized }],
|
|
252
296
|
signal,
|
|
297
|
+
// §18.6 D-TR4:轨迹元数据增补——kind=compress(上下文构建面——agent 元数据透出;
|
|
298
|
+
// depth 经 extras.traceDepth——agent.mjs 主作用域传入——compress 调用点补齐)
|
|
299
|
+
logCtx: {
|
|
300
|
+
stage: "compress", child: agent._logId, kind: "compress",
|
|
301
|
+
role: agent._role ?? null, depth: extras?.traceDepth ?? null,
|
|
302
|
+
session: agent._sessionStart ?? null, cwd: agent.cwd,
|
|
303
|
+
traces: agent.config?.traces?.enabled !== false,
|
|
304
|
+
},
|
|
253
305
|
})
|
|
254
306
|
|
|
255
307
|
applyCompression(agent, split.headEnd, split.tailStart, COMPACTION_PREFIX + summary.content)
|
|
@@ -272,7 +324,7 @@ export async function compressIfNeeded(agent, threshold, callbacks, extras = {},
|
|
|
272
324
|
*/
|
|
273
325
|
export function compressFallback(agent) {
|
|
274
326
|
const keepTail = keepTailSize(agent.provider, agent.history.length)
|
|
275
|
-
const split = splitHistory(agent.history, keepTail)
|
|
327
|
+
const split = splitHistory(agent.history, keepTail, tailBudgetTokens(agent.provider))
|
|
276
328
|
if (!split) return false
|
|
277
329
|
const tailMessages = agent.history.length - split.tailStart
|
|
278
330
|
applyCompression(agent, split.headEnd, split.tailStart, FALLBACK_NOTE)
|
|
@@ -322,142 +374,8 @@ function shrinkOversized(agent, limit = OVERSIZE_CONTENT_LIMIT) {
|
|
|
322
374
|
return shrunk
|
|
323
375
|
}
|
|
324
376
|
|
|
325
|
-
// ─── End-of-run exploration distillation
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// replaces them in the machine line, while agent._fullHistory (the human line) stays untouched.
|
|
329
|
-
|
|
330
|
-
/** Read-only knowledge tools counted as "exploration" (execute writes files → never exploration). */
|
|
331
|
-
export const EXPLORE_TOOLS = new Set([
|
|
332
|
-
"read", "grep", "glob", "ls", "code_search", "doc_search", "repo_outline",
|
|
333
|
-
])
|
|
334
|
-
|
|
335
|
-
/** Summary prompt for turning a burst of exploration results into a semantic summary. */
|
|
336
|
-
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.
|
|
337
|
-
|
|
338
|
-
Requirements:
|
|
339
|
-
- Capture WHAT was discovered, WHERE (which files / directories / symbols), and the KEY CONCLUSIONS — do not list tool calls mechanically
|
|
340
|
-
- Keep actionable facts the main agent needs to continue: code locations, function names, file paths, structure, and open questions the exploration raised
|
|
341
|
-
- Drop raw tool-output noise, repeated lines, and verbatim file dumps — keep only what must be remembered
|
|
342
|
-
- Be honest: mark anything not actually verified as "unverified"; do not present guesses as facts
|
|
343
|
-
- Use bullet points; aim for information completeness, not a hard word limit
|
|
344
|
-
|
|
345
|
-
Exploration log:
|
|
346
|
-
`
|
|
347
|
-
|
|
348
|
-
/** tool_calls name across both stored shapes ({function:{name}} and flat {name}). */
|
|
349
|
-
function toolCallName(tc) {
|
|
350
|
-
return tc?.function?.name ?? tc?.name ?? ""
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Tool that produced a tool-result message (falls back to its owner assistant's tool_call). */
|
|
354
|
-
function toolResultName(msg, ownerToolCalls) {
|
|
355
|
-
if (typeof msg?.name === "string" && msg.name) return msg.name
|
|
356
|
-
const owner = (ownerToolCalls ?? []).find((tc) => tc.id === msg?.tool_call_id)
|
|
357
|
-
return owner ? toolCallName(owner) : ""
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Find the pure-exploration "assistant(tool_calls)→tool…" pair blocks added since `start`.
|
|
362
|
-
* A block is explorable only when EVERY tool call AND every tool result in it is an exploration
|
|
363
|
-
* tool — mixed blocks (read + edit in one turn) stay untouched, or we'd orphan the edit pairing.
|
|
364
|
-
*/
|
|
365
|
-
function findExplorationBlocks(history, start) {
|
|
366
|
-
const blocks = []
|
|
367
|
-
let i = start
|
|
368
|
-
while (i < history.length) {
|
|
369
|
-
const m = history[i]
|
|
370
|
-
if (m?.role === "assistant" && Array.isArray(m.tool_calls) && m.tool_calls.length > 0) {
|
|
371
|
-
let j = i + 1
|
|
372
|
-
while (j < history.length && history[j]?.role === "tool") j++
|
|
373
|
-
const toolMsgs = history.slice(i + 1, j)
|
|
374
|
-
const allCallsExplore = m.tool_calls.every((tc) => EXPLORE_TOOLS.has(toolCallName(tc)))
|
|
375
|
-
const allResultsExplore = toolMsgs.length > 0 && toolMsgs.every((t) => EXPLORE_TOOLS.has(toolResultName(t, m.tool_calls)))
|
|
376
|
-
if (allCallsExplore && allResultsExplore) {
|
|
377
|
-
blocks.push({ start: i, end: j, messages: history.slice(i, j), toolCount: toolMsgs.length })
|
|
378
|
-
}
|
|
379
|
-
i = j
|
|
380
|
-
} else {
|
|
381
|
-
i++
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return blocks
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/** Serialize a batch of exploration messages for the summary LLM (same shape as compaction serialization). */
|
|
388
|
-
function serializeExplorationMessages(messages) {
|
|
389
|
-
const cap = 8000 // exploration results ARE the signal to distill — generous cap (quality-first, N1)
|
|
390
|
-
return messages
|
|
391
|
-
.map((m) => {
|
|
392
|
-
const toolNote = m.tool_calls ? ` [called tools: ${m.tool_calls.map(toolCallName).join(", ")}]` : ""
|
|
393
|
-
let text = ""
|
|
394
|
-
if (typeof m.content === "string") text = m.content
|
|
395
|
-
else if (Array.isArray(m.content)) text = m.content.filter((p) => p?.type === "text").map((p) => p.text ?? "").join(" ")
|
|
396
|
-
return `[${m.role}]${toolNote} ${text.slice(0, cap)}`
|
|
397
|
-
})
|
|
398
|
-
.join("\n")
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Core (shared) distillation: replace this run's pure-exploration pair blocks with a single
|
|
403
|
-
* "[Exploration summary]" note placed where the first block was. Returns a NEW history array,
|
|
404
|
-
* or null when there is nothing to shrink (<3 exploration results / LLM failure). Pairing-safe:
|
|
405
|
-
* whole assistant→tool blocks are removed, so no orphan tool_calls/tool can survive.
|
|
406
|
-
*/
|
|
407
|
-
async function distillExplorations(history, start, provider, signal) {
|
|
408
|
-
if (!Array.isArray(history) || history.length - start < 2) return null
|
|
409
|
-
const blocks = findExplorationBlocks(history, start)
|
|
410
|
-
const resultCount = blocks.reduce((n, b) => n + b.toolCount, 0)
|
|
411
|
-
if (resultCount < 3) return null
|
|
412
|
-
|
|
413
|
-
const serialized = blocks.map((b) => serializeExplorationMessages(b.messages)).join("\n")
|
|
414
|
-
|
|
415
|
-
let summary
|
|
416
|
-
try {
|
|
417
|
-
// Silent by design (D11): thinking:null and no onToken/onReasoning — this internal
|
|
418
|
-
// distillation must not stream to the frontend. signal propagates user cancellation.
|
|
419
|
-
const resp = await chat({ ...provider, thinking: null, reasoningEffort: null }, {
|
|
420
|
-
messages: [{ role: "user", content: EXPLORE_SUMMARY_PROMPT + serialized }],
|
|
421
|
-
signal,
|
|
422
|
-
})
|
|
423
|
-
summary = resp?.content
|
|
424
|
-
} catch {
|
|
425
|
-
return null // N3: never block the run's return or lose history — original results stay
|
|
426
|
-
}
|
|
427
|
-
if (!summary) return null
|
|
428
|
-
|
|
429
|
-
const drop = new Set()
|
|
430
|
-
for (const b of blocks) for (let k = b.start; k < b.end; k++) drop.add(k)
|
|
431
|
-
const note = { role: "user", content: "[Exploration summary]\n" + summary }
|
|
432
|
-
const next = []
|
|
433
|
-
let inserted = false
|
|
434
|
-
for (let k = 0; k < history.length; k++) {
|
|
435
|
-
if (drop.has(k)) {
|
|
436
|
-
if (!inserted) { next.push(note); inserted = true }
|
|
437
|
-
continue
|
|
438
|
-
}
|
|
439
|
-
next.push(history[k])
|
|
440
|
-
}
|
|
441
|
-
return next
|
|
442
|
-
}
|
|
377
|
+
// ─── End-of-run exploration distillation(2026-09-05 module-split:524 > 500 硬限——verbatim
|
|
378
|
+
// 迁至 explore-distill.mjs,语义零变——VS Code compact.mjs 同款联动;cross-repo parity 锚改指
|
|
379
|
+
// explore-distill.mjs——消费方 import 面不变(re-export))───────────────────────
|
|
443
380
|
|
|
444
|
-
|
|
445
|
-
* End-of-run exploration distillation (runAgent's final return). Shrinks the MACHINE line
|
|
446
|
-
* (agent.history) only; agent._fullHistory is never touched. Triggers when this run added ≥3
|
|
447
|
-
* exploration tool results; on LLM failure it silently keeps the original history (N3).
|
|
448
|
-
* The distillation itself is silent and never streams (D11); `callbacks.onDistilled` fires
|
|
449
|
-
* ONLY after the replacement actually lands (never on no-op/failure) — callers persist the
|
|
450
|
-
* compressed session (SEND-STALL-DISTILL §2.3).
|
|
451
|
-
*/
|
|
452
|
-
export async function summarizeRunExplorations(agent, callbacks, signal) {
|
|
453
|
-
const next = await distillExplorations(agent.history, agent._runStartHistoryLen ?? 0, agent.provider, signal)
|
|
454
|
-
if (!next) return
|
|
455
|
-
agent.history = next
|
|
456
|
-
// The machine line changed shape — the measured token baseline was for the pre-shrink context.
|
|
457
|
-
// Invalidate so the next compaction check re-estimates instead of over-counting stale history.
|
|
458
|
-
agent._lastPromptTokens = null
|
|
459
|
-
agent._usageAtLen = null
|
|
460
|
-
// The compressed machine line must reach the disk: the run's own save already happened,
|
|
461
|
-
// so without this hook the async distill would leave the session un-compressed on exit.
|
|
462
|
-
callbacks.onDistilled?.()
|
|
463
|
-
}
|
|
381
|
+
export { summarizeRunExplorations, EXPLORE_TOOLS, EXPLORE_SUMMARY_PROMPT } from "./explore-distill.mjs"
|