@thincoder/core 0.9.2 → 0.9.4
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 +76 -0
- package/README.md +1 -0
- package/agent/completion.mjs +3 -1
- package/agent/family-tools.mjs +12 -9
- package/agent/helpers.mjs +11 -2
- package/agent/run-stages.mjs +27 -5
- package/agent/setup.mjs +6 -0
- package/agent/write-gate.mjs +5 -5
- package/agent-tools/advisor-async.mjs +4 -4
- package/agent-tools/advisor.mjs +1 -1
- package/agent-tools/async-discard.mjs +1 -1
- package/agent-tools/audit-block.mjs +106 -0
- package/agent-tools/batch-lifecycle.mjs +72 -16
- package/agent-tools/batch-skeleton.mjs +69 -7
- package/agent-tools/batch.mjs +19 -6
- package/agent-tools/context.mjs +174 -0
- package/agent-tools/goal.mjs +7 -0
- package/agent-tools/parent-channel.mjs +2 -2
- package/agent-tools/plan.mjs +6 -6
- package/agent-tools/read-history.mjs +122 -24
- package/agent-tools/settings.mjs +4 -2
- package/agent-tools/subagent-async.mjs +3 -3
- package/agent-tools/subagent-spawn.mjs +29 -101
- package/agent-tools/task.mjs +11 -0
- package/agent-tools.mjs +4 -1
- package/agent.mjs +10 -4
- package/config-presets.mjs +2 -2
- package/config.mjs +1 -1
- package/context.mjs +66 -121
- package/fts-text.mjs +41 -0
- package/memory/core.mjs +4 -18
- package/memory/schema.mjs +4 -11
- package/model-specs.mjs +22 -7
- package/package.json +5 -1
- package/prompts/common.md +2 -2
- package/prompts/discipline-engineering.md +17 -2
- package/prompts/persona-engineering.md +1 -1
- package/session-gc.mjs +11 -0
- package/session-index-build.mjs +298 -0
- package/session-index-cmd.mjs +61 -0
- package/session-index-pass.mjs +95 -0
- package/session-index-query.mjs +102 -0
- package/session-index.mjs +285 -0
- package/session-slots-manifest.mjs +19 -0
- package/token-window.mjs +188 -0
- package/tools/bash.mjs +4 -15
- package/tools/execute.mjs +5 -13
- package/tools/git-checkpoint.mjs +1 -1
- package/tools/git-ext.mjs +23 -20
- package/tools/git-run.mjs +141 -0
- package/tools/git.mjs +42 -36
- package/tools/index.mjs +3 -1
- package/tools/process-tree.mjs +20 -0
- package/tools/shared.mjs +8 -4
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* 段标题定位(sectionHeaderRe)同住本档:append 段尾定位与 status/close 段内状态行定位
|
|
14
14
|
* 消费**同一个段头正则**——`## §N` 形态单源(`## §20` 不误命中 §2)。
|
|
15
|
+
*
|
|
16
|
+
* F11-C(本批):**骨架死占位枚举**(TEMPLATE_PLACEHOLDERS)与残留扫描纯函数
|
|
17
|
+
* (findPlaceholderResidue)同住本档——与骨架模板共址即判据不会与模板脱节。
|
|
15
18
|
*/
|
|
16
19
|
/**
|
|
17
20
|
* 段标题定位(`## §N` 独立标题——`## §20` 不误命中 §2)。
|
|
@@ -24,7 +27,9 @@ export const sectionHeaderRe = (seg) => new RegExp(`^## §${seg}(?=\\s|$)`, "m")
|
|
|
24
27
|
* - §4/§6 无项——主 agent 的 status 写域**仅 §1**(§4/§6 状态面走普通文档写,与 D-BR18 分工一致)。
|
|
25
28
|
* 词面纪律(最小词面):一条状态行值只带**一个**关键字——校验面(batch 工具 status)拒绝
|
|
26
29
|
* 内嵌另一冻结词的值(含「进行中…已收口」双词 ⇒ 已收口优先误冻结——2026-09-20 词面纪律)。
|
|
27
|
-
* emoji / 括注 /
|
|
30
|
+
* emoji / 括注 / 日期后缀容忍(**读侧** gate 判定 = 子串包含,同 gate——写入面 value 谓词
|
|
31
|
+
* 自本批起收紧为「剥装饰白名单后余核 = 关键词」,见 batch-lifecycle.mjs);
|
|
32
|
+
* 全词表并集 = 各段项去重集合(note 字段零命中的判据面——括注永不误触 gate)。
|
|
28
33
|
*/
|
|
29
34
|
export const STATUS_WORDS = Object.freeze({
|
|
30
35
|
1: Object.freeze({ open: "进行中", closed: "已收口" }),
|
|
@@ -42,6 +47,61 @@ export const SEGMENT_BY_ROLE = { "eng-designer": 2, "eng-coder": 5 }
|
|
|
42
47
|
/** §1 状态行前缀形态(`**状态行**:` 独立行——表格行 / 块引用行不命中;判定只认关键字)。 */
|
|
43
48
|
export const STATUS_LINE_RE = /^\s*\*\*状态行\*\*[::]\s*(.*)$/
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* 骨架死占位**枚举单源**(F11-C 判据面——create 骨架产出过的全部死占位字面):
|
|
52
|
+
* - `#<编号>` / `<板块>` = 现行骨架仍在产出的两处(台账行——填充人 = 主 agent,时点 =
|
|
53
|
+
* 建档后、本档首个 append/status 之前);
|
|
54
|
+
* - `<BATCH-ID>` / `<讨论来源>` = 改形**删除**的两处旧占位(create 不再产出;旧档残留同判)。
|
|
55
|
+
* 判据 = 枚举而非泛形正则:`<[^>]{1,40}>` 一类泛形会误杀正文合法尖括号(`Error: <message>` /
|
|
56
|
+
* 泛型 `Array<T>` / 讨论字面本身)与 `<§N 模板占位:…>` 合法暂存行(append 不删行——段内留存
|
|
57
|
+
* 是常态)——设计档 `TOOLS.md` 6.15 裁定点④。
|
|
58
|
+
*/
|
|
59
|
+
export const TEMPLATE_PLACEHOLDERS = ["<BATCH-ID>", "<讨论来源>", "#<编号>", "<板块>"]
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 死占位残留扫描(F11-C——append/status 落笔前的机检;**纯函数 · 零 fs**)。
|
|
63
|
+
* 判定域 = **档头**(档首行起至首个 `## §N` 段头前)+ **本次目标段**段内——他段模板占位不归
|
|
64
|
+
* 本段作者管(一段一作者:§2 作者扫 §5 的占位 = 越权)。
|
|
65
|
+
* @param {string} src — 档全文
|
|
66
|
+
* @param {number|string} seg — 本次目标段号(`2` / `"2"`——同 sectionHeaderRe 消费形)
|
|
67
|
+
* @returns {Array<{line: number, text: string, where: "header"|"section"}>} 残留列表(空 = 无残留);
|
|
68
|
+
* `line` = 所在判定域内 1-based 行号(档头域自档首行起 · 段域自 `## §N` 标题行起——便于定位),
|
|
69
|
+
* `text` = 命中行 trim 原文,`where` = 命中域(档头 / 目标段——错误句标注位置用)
|
|
70
|
+
*/
|
|
71
|
+
export function findPlaceholderResidue(src, seg) {
|
|
72
|
+
const out = []
|
|
73
|
+
const scan = (text, where) => {
|
|
74
|
+
text.split("\n").forEach((line, i) => {
|
|
75
|
+
if (TEMPLATE_PLACEHOLDERS.some((ph) => line.includes(ph))) out.push({ line: i + 1, text: line.trim(), where })
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
const firstSection = /^## §\d/m.exec(src)
|
|
79
|
+
scan(src.slice(0, firstSection ? firstSection.index : 0), "header")
|
|
80
|
+
const hdr = sectionHeaderRe(seg).exec(src)
|
|
81
|
+
if (hdr) {
|
|
82
|
+
const nextRe = /^## §\d/gm
|
|
83
|
+
nextRe.lastIndex = hdr.index + hdr[0].length
|
|
84
|
+
const next = nextRe.exec(src)
|
|
85
|
+
scan(src.slice(hdr.index, next ? next.index : src.length), "section")
|
|
86
|
+
}
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 占位残留拒绝句(F11-C——append / status 两个挂点共用单源文案;逐行列残留:位置标注 + 行号 + 原文)。
|
|
92
|
+
* 错误句前缀 = `batch:`(本批新增错误面——与 append 迁移面的 `batch_segment:` 旧面不混)。
|
|
93
|
+
* @param {Array<{line:number,text:string,where:string}>} residues — findPlaceholderResidue 的返回值(非空)
|
|
94
|
+
* @returns {string} 完整拒绝句(调用方 throw new Error(...))
|
|
95
|
+
*/
|
|
96
|
+
export function placeholderResidueError(residues) {
|
|
97
|
+
const where = (r) => (r.where === "header" ? "档头" : "目标段")
|
|
98
|
+
const list = residues.map((r) => ` ${where(r)} line ${r.line}: ${r.text}`).join("\n")
|
|
99
|
+
return "batch: 骨架死占位残留 — the batch record still carries skeleton placeholders (档头 or your target section). " +
|
|
100
|
+
"Fill them before writing: create ⇒ the main agent fills the header's 台账编号 / 板块 placeholders ⇒ the record's first append/status opens. " +
|
|
101
|
+
`Nothing was written. Residue:\n${list}`
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
45
105
|
/**
|
|
46
106
|
* §1 状态行解析(冻结门与 lifecycle 共用的**单源解析器**——原 batch-segment.mjs 逐字迁移,
|
|
47
107
|
* 字面「已收口/进行中」改为消费 STATUS_WORDS[1],行为零变):解析对象 = §1 段内
|
|
@@ -68,16 +128,18 @@ export function readBatchStatusLine(src) {
|
|
|
68
128
|
/**
|
|
69
129
|
* create 骨架模板(BATCH-RECORD.md §4.10 行为规格的**代码单源**——红线:模板只覆盖骨架与
|
|
70
130
|
* 占位,不模板化内容):`## §1–§6` 段头(含职责署名)+ 档头 boilerplate(六段一段一作者句 +
|
|
71
|
-
* 编制行 +
|
|
131
|
+
* 编制行 + 台账/前情指针行)+ §1 段内**占位状态行**(含 gate 合法关键字「进行中」——建档即过
|
|
72
132
|
* gate;整行不含「已收口」——gate 已收口优先,占位行不得携带冻结词)+ 各段模板子标题占位。
|
|
73
|
-
* 已知实参替换:date / topic / prev(§4.11
|
|
74
|
-
*
|
|
133
|
+
* 已知实参替换:date / topic / **source** / prev(§4.11 参数面 + F11-B——source 必填、prev 传
|
|
134
|
+
* 入值过幂等剥前缀,归一化在 lifecycle 面);档头 `#<编号>` / `<板块>` 仍保持占位字面(台账
|
|
135
|
+
* 登记 = 主 agent 既有义务,create 不代建——填充时点 = 建档后、本档首个 append/status 之前)。
|
|
136
|
+
* 本批改形删除(F11-B):旧 `# …(<BATCH-ID>)` 后缀与 `来源 = <讨论来源>` 占位实参化。
|
|
75
137
|
*/
|
|
76
|
-
export function batchSkeleton({ date, topic, prev }) {
|
|
138
|
+
export function batchSkeleton({ date, topic, source, prev }) {
|
|
77
139
|
return [
|
|
78
|
-
`# ${date} · ${topic}
|
|
140
|
+
`# ${date} · ${topic}`,
|
|
79
141
|
"> 六段 append-only,一段一作者:§1 讨论(主 agent)· §2 批次任务与设计(eng-designer)· §3 设计评审(评审子代理)· §4 用户批准(主 agent)· §5 实施记录(eng-coder)· §6 验证与收口(父代理)。",
|
|
80
|
-
`> 编制:主 agent · ${date} · 来源 =
|
|
142
|
+
`> 编制:主 agent · ${date} · 来源 = ${source}。`,
|
|
81
143
|
`> 台账 = #<编号>(<板块> · 归批)。前情 = ${prev}。`,
|
|
82
144
|
"## §1 讨论(主 agent)",
|
|
83
145
|
`**状态行**:🔄 ${STATUS_WORDS[1].open}(…)`,
|
package/agent-tools/batch.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
|
31
31
|
import { resolve } from "node:path"
|
|
32
32
|
|
|
33
33
|
import { docRootBase, docRootPaths, readManifest } from "../manifest.mjs"
|
|
34
|
-
import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe } from "./batch-skeleton.mjs"
|
|
34
|
+
import { SEGMENT_BY_ROLE, readBatchStatusLine, sectionHeaderRe, findPlaceholderResidue, placeholderResidueError } from "./batch-skeleton.mjs"
|
|
35
35
|
import { closeBatchRecord, createBatchRecord, findInFlightBatch, statusBatchRecord } from "./batch-lifecycle.mjs"
|
|
36
36
|
|
|
37
37
|
export { SEGMENT_BY_ROLE }
|
|
@@ -193,7 +193,7 @@ function insertIntoSection(src, seg, text) {
|
|
|
193
193
|
* eng-coder → §5(迁移错误面,"batch_segment:" 前缀**逐字保持**);
|
|
194
194
|
* - 目标定位(D-BR21):depth-0 path 可选(缺省 = findInFlightBatch 唯一在飞批);子代理/评审
|
|
195
195
|
* 传 path ⇒ 拒,目标 = spawn/实例绑定;
|
|
196
|
-
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / 插入 / 记账 / 回执)逐字保持原语义。
|
|
196
|
+
* - 其余(gate / text 校验 / 剥凭证 / 骨架保护 / **死占位机检(F11-C)** / 插入 / 记账 / 回执)逐字保持原语义。
|
|
197
197
|
*/
|
|
198
198
|
function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
199
199
|
const agent = ctx?.agent ?? {}
|
|
@@ -254,6 +254,10 @@ function appendBatchRecord({ args, ctx, review, batchDoc, onWritten }) {
|
|
|
254
254
|
if (!body.trim()) {
|
|
255
255
|
throw new Error("batch_segment: nothing to append — the text is empty after credential stripping (credential values never reach the record).")
|
|
256
256
|
}
|
|
257
|
+
// F11-C(挂点:gate / text 校验后 · insertIntoSection 前):档头 + 目标段死占位残留 ⇒ 拒
|
|
258
|
+
// (他段占位不拦——一段一作者;骨架枚举单源 = skeleton 档 TEMPLATE_PLACEHOLDERS)。
|
|
259
|
+
const residues = findPlaceholderResidue(src, n)
|
|
260
|
+
if (residues.length) throw new Error(placeholderResidueError(residues))
|
|
257
261
|
const { written, roundN } = insertIntoSection(src, n, body)
|
|
258
262
|
writeFileSync(abs, written)
|
|
259
263
|
// #84:记账面按端注入(缺省 no-op = CLI 语义零行为变——端装配经 configureBatchSegment 覆盖)。
|
|
@@ -272,11 +276,12 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
272
276
|
name: "batch",
|
|
273
277
|
description:
|
|
274
278
|
"Batch-record lifecycle tool (一段一作者 — six append-only sections, one author each). " +
|
|
275
|
-
"actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic) · " +
|
|
279
|
+
"actions: create (depth-0 only — write a new six-section skeleton record; pass path + topic + source) · " +
|
|
276
280
|
"append (add text to the END of YOUR section — existing lines are never rewritten or deleted) · " +
|
|
277
|
-
"status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry) · " +
|
|
281
|
+
"status (update YOUR section's `**状态行**:` line — the value must carry exactly ONE legal keyword of your section's STATUS_WORDS entry, decorated only by surrounding symbols / a trailing ISO date / whitespace; prose goes to the separate `note` field, which lands as a parenthetical) · " +
|
|
278
282
|
"close (depth-0 only — freeze the record: §1 → 「已收口 <date>」, every further write is refused). " +
|
|
279
283
|
"Identity fixes what you may write (段白名单): main agent (depth 0) — append §1/§4/§6, status §1 only, create/close; eng-designer — §2; eng-coder — §5; design review (review binding) — §3. A write outside your own section is refused. " +
|
|
284
|
+
"Skeleton placeholders (dead literals — `#<编号>` / `<板块>` and the older `<BATCH-ID>` / `<讨论来源>`) in the record header or in your target section block append/status: fill them first (the main agent fills the header right after create); `<§N 模板占位:…>` template lines are legal and never blocked. " +
|
|
280
285
|
"Target record: sub-agents and design reviews have NO path parameter in practice — the record arrives via the spawn binding / the review instance key (passing path is refused); depth-0 MAY pass path (若传则须可读), omitting it picks the unique in-flight record (0 or ≥2 in flight ⇒ refuse — pass path). " +
|
|
281
286
|
"A design review's append is stamped by the tool with a `### 轮次 N(评审子代理)` heading — N is tool-counted; do not write your own heading (it would be dropped). " +
|
|
282
287
|
"Credential values are stripped mechanically before writing (never write a token or designId value). " +
|
|
@@ -303,19 +308,27 @@ export function batchTool(batchDoc = null, { review = false } = {}) {
|
|
|
303
308
|
},
|
|
304
309
|
value: {
|
|
305
310
|
type: "string",
|
|
306
|
-
description: "status only — the new status-line value (single line;
|
|
311
|
+
description: "status only — the new status-line value (single line; exactly one legal keyword of your section's STATUS_WORDS entry: §1 进行中/已收口 · §2 设计完成 · §3 评审完成 · §5 实施完成 — plus decoration only: surrounding symbols / a trailing ISO date / whitespace; prose goes to `note`).",
|
|
312
|
+
},
|
|
313
|
+
note: {
|
|
314
|
+
type: "string",
|
|
315
|
+
description: "status only — prose that lands as a parenthetical on the status line (`**状态行**:进行中(note)`). Single line; must carry no STATUS_WORDS keyword (those go in `value`) and no skeleton placeholders. Omit for a plain value.",
|
|
307
316
|
},
|
|
308
317
|
topic: {
|
|
309
318
|
type: "string",
|
|
310
319
|
description: "create only — the batch subject word shared by the record header (keep the file name aligned with it).",
|
|
311
320
|
},
|
|
321
|
+
source: {
|
|
322
|
+
type: "string",
|
|
323
|
+
description: "create only — REQUIRED: the record's origin line (来源 = …; single line). It fills the header's 编制 line; without it the skeleton's placeholder would block the record's first append/status (topic-style fail-closed). Nothing is written when it is missing.",
|
|
324
|
+
},
|
|
312
325
|
date: {
|
|
313
326
|
type: "string",
|
|
314
327
|
description: "create only — the record date (YYYY-MM-DD); defaults to today.",
|
|
315
328
|
},
|
|
316
329
|
prev: {
|
|
317
330
|
type: "string",
|
|
318
|
-
description: "create only — the predecessor pointer line (前情); defaults to 无(独立批).",
|
|
331
|
+
description: "create only — the predecessor pointer line (前情); a leading `前情 = ` prefix is stripped automatically (idempotent — repeated prefixes included); defaults to 无(独立批).",
|
|
319
332
|
},
|
|
320
333
|
},
|
|
321
334
|
required: ["action"],
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-tools/context.mjs — `context` 工具(CONTEXT-COMPACTION.md §6.16 · context-tool 批 2026-09-21)。
|
|
3
|
+
* 单一工具三操作(用户定形 F-CC5):`stats`(感知 F-CC1)· `prune`(噪声清理 F-CC3)· `compact`(主动压缩 F-CC2,携 focus + 自动附任务/目标 + 可回查锚——裁令 A/D)。
|
|
4
|
+
* 内联 description(**不新增 tool-docs 档**——24 档族计数零动);装配面 = 登记册 `agent-tools.mjs`(#83)+ `agent/family-tools.mjs` 的 depth-0 段(`depthOnly`)——两端自动同表;**depth>0 不给**(§6.16.6)。
|
|
5
|
+
* 本档同时是**注记文案 / 轻推行 / 回执文案的单源**(核与 VSC 的 run-stages 消费点经动态 import 取用——零新增静态边)。
|
|
6
|
+
* 机械面:`stats` 不新增第三口径(阈值取本回合判定值;百分比走 `historyPercent` 与状态行同式);`prune` = 合格集(`token-window.mjs` 单源)+「原位换内容」应用面(`context.mjs`);`compact` **当次零执行**(只登记 `agent._pendingCompact`,由下一安全点消费——§6.16.2)。
|
|
7
|
+
*/
|
|
8
|
+
import { contextUsage, estimateTokens, collectStaleToolOutputs, historyPercent, PRUNE_MIN_TOKENS } from "../token-window.mjs"
|
|
9
|
+
import { pruneStaleToolOutputs, COMPRESS_FAILURE_LIMIT } from "../context.mjs"
|
|
10
|
+
import { slotPath } from "../session-slots.mjs"
|
|
11
|
+
|
|
12
|
+
/** 轻推行前缀(单活体去重判据——§6.16.5「同前缀旧行先滤」)。 */
|
|
13
|
+
export const CONTEXT_NUDGE_PREFIX = "[System reminder: task/goal changed —"
|
|
14
|
+
|
|
15
|
+
/** 压缩结果注记前缀(机器行族——同 `[System reminder:` 门:既有 `isRealUserMsg` / plan 节律等消费方按该族识别机器行)——成功**不落**注记。 */
|
|
16
|
+
export const CONTEXT_NOTE_PREFIX = "[System reminder: context compact"
|
|
17
|
+
|
|
18
|
+
/** 压缩 no-op 注记(§6.16.2 回执面②——强制面无可压:短历史无中段)。 */
|
|
19
|
+
export function compactNoopNote() {
|
|
20
|
+
return `${CONTEXT_NOTE_PREFIX} did nothing right now (no middle section — the history is too short to summarize). The context is unchanged.]`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 压缩失败注记(§6.16.2 回执面②——模型面告知缺口;失败链本体复用既有:连续 3 次降级确定性截断)。 */
|
|
24
|
+
export function compactFailureNote(message, attempt) {
|
|
25
|
+
return `${CONTEXT_NOTE_PREFIX} failed (attempt ${attempt} of ${COMPRESS_FAILURE_LIMIT}): ${message}]`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** 轻推锚(§6.16.5):active goal 目标 ⇒ in_progress 任务标题 ⇒ 首个 pending 标题 ⇒ `(none set)`。 */
|
|
29
|
+
function nudgeAnchor(agent) {
|
|
30
|
+
if (agent?.goal?.status === "active" && agent.goal.objective) return agent.goal.objective
|
|
31
|
+
const tasks = agent?.tasks ?? []
|
|
32
|
+
const inProgress = tasks.find((t) => t.status === "in_progress")
|
|
33
|
+
if (inProgress) return inProgress.title
|
|
34
|
+
const pending = tasks.find((t) => t.status === "pending")
|
|
35
|
+
if (pending) return pending.title
|
|
36
|
+
return "(none set)"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 方向转换轻推(F-CC4 · §6.16.5):task/goal 变更后直推**一行**机器行(`transient: true`,非命令)。
|
|
41
|
+
* 去重 = **单活体行**:同前缀旧行先滤再推一行 ⇒ 任意次数变更后恒**恰一行**(in-place splice 保
|
|
42
|
+
* 数组引用——VSC 面板持同一数组:`agent.history = <新数组>` 会让端侧局部引用失效,先例 = D-CC24
|
|
43
|
+
* 同一顾虑)。压缩重建时该行无特殊处理(一次建议——落入中段即随摘要退场、落入 tail 则保留,良性)。
|
|
44
|
+
* 深度门由调用方落(`ctx.depth === 0`——子代理拿不到 `context`,提示它拿不到的工具即噪音)。
|
|
45
|
+
*/
|
|
46
|
+
export function pushContextNudge(agent) {
|
|
47
|
+
const history = agent.history
|
|
48
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
49
|
+
const m = history[i]
|
|
50
|
+
if (m?.role === "user" && typeof m.content === "string" && m.content.startsWith(CONTEXT_NUDGE_PREFIX)) {
|
|
51
|
+
history.splice(i, 1)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
history.push({
|
|
55
|
+
role: "user",
|
|
56
|
+
content: `${CONTEXT_NUDGE_PREFIX} current objective: ${nudgeAnchor(agent)}. ` +
|
|
57
|
+
`If the earlier context no longer serves this direction, you may compact it (context tool, action="compact" with a focus) — the decision is yours.]`,
|
|
58
|
+
transient: true,
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** stats 报面(§6.16.4 逐行定稿——机判可解析;三段 / 工具输出小计 / 状态行百分比 / 压缩履历)。 */
|
|
63
|
+
function statsReport(agent) {
|
|
64
|
+
const usage = contextUsage(agent, agent?._ctxBasis?.overhead ?? {})
|
|
65
|
+
const stale = collectStaleToolOutputs(agent?.history ?? [], agent?.provider)
|
|
66
|
+
let toolOut = 0
|
|
67
|
+
let toolCount = 0
|
|
68
|
+
for (const m of agent?.history ?? []) {
|
|
69
|
+
if (m?.role !== "tool") continue
|
|
70
|
+
toolCount++
|
|
71
|
+
toolOut += estimateTokens([m])
|
|
72
|
+
}
|
|
73
|
+
const pct = Math.round((usage.total / usage.threshold) * 100)
|
|
74
|
+
const toGo = Math.max(0, usage.threshold - usage.total) // 已越阈值 ⇒ 0(自动压缩将在下一安全点接手)
|
|
75
|
+
const info = agent?._lastCompressInfo
|
|
76
|
+
const compaction = !info ? "none"
|
|
77
|
+
: info.mode === "summary" ? `last summary freed ${info.tokensFreed} tokens`
|
|
78
|
+
: "last fallback truncation"
|
|
79
|
+
return [
|
|
80
|
+
`Context ≈${usage.total} tokens vs compaction threshold ${usage.threshold} (${pct}% — ${toGo} to go); window ${usage.window}`,
|
|
81
|
+
`Segments ≈: system ${usage.system} · tools ${usage.tools} · history ${usage.history} (basis: ${usage.basis})`,
|
|
82
|
+
`History: tool outputs ${toolOut} tokens in ${toolCount} msgs · prunable stale ${stale.tokens} tokens in ${stale.indexes.length} msgs · protected tail ${stale.tailCount} msgs`,
|
|
83
|
+
`Status line: context ${historyPercent(agent?.history ?? [], agent?.provider)}% (${usage.history} history estimate / ${usage.window} window)`,
|
|
84
|
+
`Compaction so far: ${compaction} · failures ${agent?._compressFailures ?? 0}/${COMPRESS_FAILURE_LIMIT}`,
|
|
85
|
+
].join("\n")
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** prune 回执(§6.16.3:计数 + 扫过候选 / 保护尾内保留 / 门槛下跳过 + 记录面零改)。 */
|
|
89
|
+
function pruneReceipt(agent) {
|
|
90
|
+
const r = pruneStaleToolOutputs(agent)
|
|
91
|
+
const scan = `(scanned ${r.candidates} tool result(s): ${r.tailKept} inside the protected tail, ${r.belowMin} below the ${PRUNE_MIN_TOKENS}-token floor)`
|
|
92
|
+
if (r.pruned === 0) {
|
|
93
|
+
return `Nothing to prune — no stale tool output matched ${scan}. The context and the session record are unchanged.`
|
|
94
|
+
}
|
|
95
|
+
return `Pruned ${r.pruned} stale tool output(s) ≈${r.freed} tokens freed ${scan}. ` +
|
|
96
|
+
`Tool pairing is intact and the session record is unchanged — the full text is still on disk; re-run a tool if you need its output again.`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 可回查锚(父侧裁令 A · §6.16.7 两分支逐字):回执尾段给出「压掉的可一步取回」的坐标与调用形。
|
|
101
|
+
* 取回面 = `read_history` 既有 `path=`(**不新建通道**);未绑定槽(会话首次保存前 / VSC 端面板槽
|
|
102
|
+
* 不在 agent 上)⇒ `cwd:` 发现面(等效取回,已登记的端差)。
|
|
103
|
+
*/
|
|
104
|
+
function retrievabilityAnchor(agent) {
|
|
105
|
+
const slot = agent?._slot
|
|
106
|
+
// 已绑定槽分支需 cwd 才能给槽文件坐标(cwd 缺省 ⇒ 退 cwd 发现面——工具内不抛)
|
|
107
|
+
if (slot != null && agent?.cwd) {
|
|
108
|
+
const file = slotPath(agent.cwd, slot)
|
|
109
|
+
return `Full record (never compacted): ${file} — read it back with: read_history path="${file}" (add keyword / since / role filters to target it).`
|
|
110
|
+
}
|
|
111
|
+
return `Full record: this session's file is created on first save — list this project's sessions with: read_history path="cwd:${agent.cwd}", then copy the listed path into path= to query it.`
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** compact 回执(§6.16.2 回执面①:排队事实 + 替换明示 + focus 回显 + 落点说明 + 可回查锚)。 */
|
|
115
|
+
function compactReceipt(agent, focus, replaced) {
|
|
116
|
+
return [
|
|
117
|
+
`Compaction queued${replaced ? " (replaces the request queued earlier — only the latest focus is used)" : ""} — ` +
|
|
118
|
+
`it runs at the next safe point, before the next request; the current exchange is never cut mid-flight.`,
|
|
119
|
+
`Focus: ${focus}`,
|
|
120
|
+
`The summary is weighted toward that focus; the current task list and goal are attached automatically. ` +
|
|
121
|
+
`Same summary chain, same panel, same failure handling as the automatic compaction.`,
|
|
122
|
+
retrievabilityAnchor(agent),
|
|
123
|
+
].join("\n")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const contextTool = {
|
|
127
|
+
name: "context",
|
|
128
|
+
description:
|
|
129
|
+
"Manage your own context window — see how full it is, drop stale tool output, or compact the earlier " +
|
|
130
|
+
"conversation into a summary at a moment of your own choosing.\n" +
|
|
131
|
+
"- action='stats': current usage — total vs the compaction threshold, per-segment shares, and how much " +
|
|
132
|
+
"is prunable / reclaimable.\n" +
|
|
133
|
+
"- action='prune': drop the CONTENT of stale tool results (older than the protected tail) and replace it " +
|
|
134
|
+
"with a short stub — tool pairing stays intact, the session record keeps the full text, and the tool can " +
|
|
135
|
+
"simply be re-run.\n" +
|
|
136
|
+
"- action='compact': compact the earlier conversation now, with 'focus' — write what the UPCOMING work " +
|
|
137
|
+
"needs (1–3 sentences: the goal, the files, the constraints that must survive). The summary is written to " +
|
|
138
|
+
"serve that focus; the current task list and goal are attached automatically. It runs at the next safe " +
|
|
139
|
+
"point (before the next request — never mid-exchange) through the same machine as the automatic " +
|
|
140
|
+
"compaction: same summary chain, same panel, same failure chain (3 consecutive failures degrade to a " +
|
|
141
|
+
"deterministic truncation). The receipt carries this session's record path, so nothing is lost for good.\n" +
|
|
142
|
+
"Nothing here runs on its own — the automatic threshold compaction stays in place as a fallback; whether " +
|
|
143
|
+
"to compact is your call. Errors come back as 'Error: ...' and change nothing.\n" +
|
|
144
|
+
"Parameters:\n" +
|
|
145
|
+
"- action (required): stats | prune | compact\n" +
|
|
146
|
+
"- focus (required for compact): what the upcoming work needs — the summary is weighted toward it",
|
|
147
|
+
parameters: {
|
|
148
|
+
type: "object",
|
|
149
|
+
properties: {
|
|
150
|
+
action: { type: "string", enum: ["stats", "prune", "compact"], description: "stats — usage report; prune — drop stale tool-output content; compact — summarize the earlier conversation now." },
|
|
151
|
+
focus: { type: "string", description: "compact only (required): what the upcoming work needs — 1–3 sentences. The summary is weighted toward it." },
|
|
152
|
+
},
|
|
153
|
+
required: ["action"],
|
|
154
|
+
},
|
|
155
|
+
readonly: true, // 只动机内状态(同 task / goal 先例)——planMode 放行、只读角色过滤放行、无权限问询
|
|
156
|
+
async execute(args, ctx) {
|
|
157
|
+
const agent = ctx.agent
|
|
158
|
+
const action = args?.action
|
|
159
|
+
if (action === "stats") return statsReport(agent)
|
|
160
|
+
if (action === "prune") return pruneReceipt(agent)
|
|
161
|
+
if (action === "compact") {
|
|
162
|
+
const focus = typeof args?.focus === "string" ? args.focus.trim() : ""
|
|
163
|
+
// fail-closed(§6.16.1 参数面):schema 层 `focus` 非 required ⇒ 运行期判——不排队、零副作用
|
|
164
|
+
if (!focus) {
|
|
165
|
+
return "Error: 'focus' is required for action='compact' — write 1–3 sentences on what the upcoming work needs " +
|
|
166
|
+
"(the goal, the files, the constraints that must survive). Nothing was queued."
|
|
167
|
+
}
|
|
168
|
+
const replaced = agent._pendingCompact != null
|
|
169
|
+
agent._pendingCompact = { focus, at: Date.now() } // 单槽:重复调用后者覆盖前者(回执明示)
|
|
170
|
+
return compactReceipt(agent, focus, replaced)
|
|
171
|
+
}
|
|
172
|
+
return `Error: unknown action '${action}' — valid actions: stats | prune | compact`
|
|
173
|
+
},
|
|
174
|
+
}
|
package/agent-tools/goal.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// F-CC4(CONTEXT-COMPACTION.md §6.16.5):goal 任一状态变更 ⇒ 方向转换轻推(本档只调用;文案/去重单源 = agent-tools/context.mjs)
|
|
2
|
+
import { pushContextNudge } from "./context.mjs"
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* goal tool: lifecycle management for long-running autonomous goals (completion contract).
|
|
3
6
|
* Three states: active / complete / blocked. Completion must pass a verify evidence threshold;
|
|
@@ -28,6 +31,7 @@ export const goalTool = {
|
|
|
28
31
|
const agent = ctx.agent
|
|
29
32
|
if (args.action === "cancel") {
|
|
30
33
|
agent.goal = null
|
|
34
|
+
if (ctx.depth === 0) pushContextNudge(agent) // F-CC4(depth-0 门)
|
|
31
35
|
return "Goal cancelled. If the goal was blocked or impossible, explain why in your next message — the user can clarify, adjust scope, or confirm cancellation."
|
|
32
36
|
}
|
|
33
37
|
if (args.action === "set") {
|
|
@@ -43,6 +47,7 @@ export const goalTool = {
|
|
|
43
47
|
turnsUsed: 0,
|
|
44
48
|
_blockTally: null, // { reason, count } — consecutive count of the same blocking condition (for blocked audit)
|
|
45
49
|
}
|
|
50
|
+
if (ctx.depth === 0) pushContextNudge(agent) // F-CC4(depth-0 门)
|
|
46
51
|
return `Goal set: ${agent.goal.objective}\nDone when: ${agent.goal.criteria}\nThe system will inject goal status every turn. Completion and blocked claims are audited — see the reminders.`
|
|
47
52
|
}
|
|
48
53
|
if (!agent.goal || agent.goal.status !== "active") {
|
|
@@ -100,6 +105,7 @@ Has this goal been achieved? Answer ONLY "YES" or "NO" followed by a one-sentenc
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
agent.goal.status = "complete"
|
|
108
|
+
if (ctx.depth === 0) pushContextNudge(agent) // F-CC4(depth-0 门)
|
|
103
109
|
return `Goal verified complete ✓: ${agent.goal.objective}\nIn your next message, summarize the evidence (what check ran, what it showed) — the user should be able to audit this claim.`
|
|
104
110
|
}
|
|
105
111
|
if (args.action === "blocked") {
|
|
@@ -112,6 +118,7 @@ Has this goal been achieved? Answer ONLY "YES" or "NO" followed by a one-sentenc
|
|
|
112
118
|
return `Blocked not accepted yet (${count}/3 for this condition). Try a genuinely different approach first; report blocked only if the same condition stops you ${3 - count} more time(s).`
|
|
113
119
|
}
|
|
114
120
|
agent.goal.status = "blocked"
|
|
121
|
+
if (ctx.depth === 0) pushContextNudge(agent) // F-CC4(depth-0 门)
|
|
115
122
|
return `Goal marked blocked after 3 attempts: ${args.reason}\nExplain the blocker to the user in your next message — what you tried, and what you need (clarification, permission, a decision).`
|
|
116
123
|
}
|
|
117
124
|
return `Error: unknown action '${args.action}'.`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* parent-channel.mjs — 子代理上行通道(子 → 父 在飞提问 / 上报)(批 SUBAGENT-UPSTREAM-CHANNEL;
|
|
3
|
-
* 设计权威 = `docs/core/design/AGENT-LOOP-
|
|
3
|
+
* 设计权威 = `docs/core/design/AGENT-LOOP-UPSTREAM.md` §6.27;需求 = `docs/core/requirements/AGENT-LOOP.md` §4.12)。
|
|
4
4
|
*
|
|
5
5
|
* 补上子代理通信的第三象限:既有两象限 = 父→子在飞(`subagent action:'send'`)+ 子→父**终态**
|
|
6
6
|
* (报告 → settle → digest);本档 = 子→父**在飞**——子代理运行中向父(spawn 方)发一条决策级
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* ≤ `UPSTREAM_MSG_MAX` 且父队列 ≤ `UPSTREAM_QUEUE_MAX`(超限 = 工具**明确报错**,不静默丢)。
|
|
31
31
|
* 射程纪律(两问自检 + 正负清单)在提示词面(§6.27.8),机制面不新增语义判定。
|
|
32
32
|
*
|
|
33
|
-
* 显示面单点(F-UC8 · AGENT-LOOP-
|
|
33
|
+
* 显示面单点(F-UC8 · AGENT-LOOP-UPSTREAM.md §6.27.12.13 ②):`upstreamAskLabelVars(carrier)`——CLI / VSC
|
|
34
34
|
* 两端共用的提示行携参(队首 ask 的 `from` + 单行归一截断的 `message`);两端各持一份 = 漂移源(D2)。
|
|
35
35
|
*
|
|
36
36
|
* 模块图:静态 import 核单点 `async-settle.mjs`(`carrierField` / `getAsyncPool` /
|
package/agent-tools/plan.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* plan tool: enter/exit plan mode.
|
|
3
3
|
* In plan mode only read-only tools are allowed — explore code, design solutions, no code writing.
|
|
4
|
-
*
|
|
4
|
+
* Exit plan mode to present the plan for the user's approval; implement only after they approve.
|
|
5
5
|
*
|
|
6
6
|
* Reminder cadence (kimi-code style): while plan mode is active the agent loop
|
|
7
7
|
* re-injects reminders — sparse every 2 turns, full every 5 turns or when the
|
|
@@ -19,8 +19,8 @@ const PLAN_SPARSE_REMINDER =
|
|
|
19
19
|
"Design the solution, then call plan with action='exit' for user approval.]"
|
|
20
20
|
|
|
21
21
|
const PLAN_EXIT_REMINDER =
|
|
22
|
-
"[System reminder: plan mode is now OFF.
|
|
23
|
-
"
|
|
22
|
+
"[System reminder: plan mode is now OFF. Present your plan to the user " +
|
|
23
|
+
"and wait for their explicit approval before implementing — no need for a task list (the plan already covered that).]"
|
|
24
24
|
|
|
25
25
|
/** Turns between reminder re-injections while plan mode is active */
|
|
26
26
|
const SPARSE_INTERVAL = 2
|
|
@@ -41,7 +41,7 @@ const isPlanReminder = (r) => r === PLAN_FULL_REMINDER || r === PLAN_SPARSE_REMI
|
|
|
41
41
|
/**
|
|
42
42
|
* ENG-PLAN-EXCLUSION(FR31 ③ / KD10——单点复用):工程模式 ⇒ `planMode` 恒 false。
|
|
43
43
|
* 清 `planMode` + 两 reminder 计数 + **未注入的 plan 提示语**(`_pendingReminders` 中的三条
|
|
44
|
-
* plan 提醒——否则排队的 `PLAN_EXIT_REMINDER`「
|
|
44
|
+
* plan 提醒——否则排队的 `PLAN_EXIT_REMINDER`「Present your plan … wait for their explicit approval …」会在模式翻转后
|
|
45
45
|
* 落地,与工程链条打架)。五个挂点复用本函数:三翻转(核 `eng` 工具 / CLI `/eng` / VSC 面板开关)
|
|
46
46
|
* + 两恢复(CLI `applySession` / VSC `applySlotSessionState`)。
|
|
47
47
|
* @param {object} agent — 会话 agent(就地改写)
|
|
@@ -89,7 +89,7 @@ export function planReminderForTurn(agent, userMessageSince) {
|
|
|
89
89
|
export const planTool = {
|
|
90
90
|
name: "plan",
|
|
91
91
|
description:
|
|
92
|
-
"Enter or exit plan mode. In plan mode you are restricted to READ-ONLY tools: read files, search code, run read-only shell commands. Use plan mode before complex multi-step tasks — explore the codebase, design the architecture, present a plan to the user.
|
|
92
|
+
"Enter or exit plan mode. In plan mode you are restricted to READ-ONLY tools: read files, search code, run read-only shell commands. Use plan mode before complex multi-step tasks — explore the codebase, design the architecture, present a plan to the user. Exit plan mode to present the plan for the user's approval; implement only after they approve. For simple single-file edits, skip plan mode and just make the change.",
|
|
93
93
|
parameters: {
|
|
94
94
|
type: "object",
|
|
95
95
|
properties: {
|
|
@@ -106,7 +106,7 @@ export const planTool = {
|
|
|
106
106
|
ctx.agent._planTurnsSinceReminder = 0
|
|
107
107
|
ctx.agent._pendingReminders = ctx.agent._pendingReminders ?? []
|
|
108
108
|
ctx.agent._pendingReminders.push(PLAN_EXIT_REMINDER)
|
|
109
|
-
return "Plan mode exited.
|
|
109
|
+
return "Plan mode exited. Present your plan to the user and wait for their explicit approval before writing any code."
|
|
110
110
|
}
|
|
111
111
|
if (args.action !== "enter") {
|
|
112
112
|
return `Error: unknown action "${args.action}". Use "enter" or "exit".`
|