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.
Files changed (158) hide show
  1. package/CHANGELOG.md +78 -2
  2. package/README.md +3 -3
  3. package/bin/thincoder.mjs +88 -19
  4. package/package.json +4 -3
  5. package/src/acp/bridge.mjs +135 -26
  6. package/src/advisor/messages.mjs +57 -4
  7. package/src/advisor/run.mjs +119 -79
  8. package/src/advisor.mjs +34 -7
  9. package/src/agent/completion.mjs +17 -11
  10. package/src/agent/dispatch.mjs +182 -22
  11. package/src/agent/helpers.mjs +71 -4
  12. package/src/agent/record-results.mjs +46 -10
  13. package/src/agent/run-stages.mjs +227 -0
  14. package/src/agent/setup-reminders.mjs +62 -0
  15. package/src/agent/setup.mjs +107 -20
  16. package/src/agent/spawn-child.mjs +54 -4
  17. package/src/agent-tools/advisor-async.mjs +456 -0
  18. package/src/agent-tools/advisor.mjs +133 -109
  19. package/src/agent-tools/async-settle.mjs +191 -0
  20. package/src/agent-tools/consult.mjs +154 -104
  21. package/src/agent-tools/design-token.mjs +104 -0
  22. package/src/agent-tools/eng.mjs +26 -30
  23. package/src/agent-tools/escalate-async.mjs +286 -0
  24. package/src/agent-tools/goal.mjs +11 -1
  25. package/src/agent-tools/read-history.mjs +284 -0
  26. package/src/agent-tools/recent-changes.mjs +2 -1
  27. package/src/agent-tools/settings.mjs +152 -0
  28. package/src/agent-tools/skill.mjs +2 -1
  29. package/src/agent-tools/subagent-actions.mjs +470 -0
  30. package/src/agent-tools/subagent-async.mjs +382 -0
  31. package/src/agent-tools/subagent-panel.mjs +153 -0
  32. package/src/agent-tools/subagent-run.mjs +202 -0
  33. package/src/agent-tools/subagent-scheduler.mjs +343 -0
  34. package/src/agent-tools/subagent-spawn.mjs +406 -0
  35. package/src/agent-tools/subagent.mjs +203 -377
  36. package/src/agent-tools/task.mjs +4 -3
  37. package/src/agent-tools/timer.mjs +9 -4
  38. package/src/agent-tools/verify.mjs +198 -238
  39. package/src/agent-tools.mjs +1 -0
  40. package/src/agent.mjs +145 -242
  41. package/src/auto-think.mjs +14 -0
  42. package/src/cli/distill-command.mjs +10 -4
  43. package/src/cli/make-agent.mjs +4 -1
  44. package/src/cli/memory-command.mjs +2 -1
  45. package/src/cli/permission.mjs +8 -1
  46. package/src/cli/setup-wizard.mjs +17 -12
  47. package/src/config.mjs +61 -8
  48. package/src/context.mjs +81 -163
  49. package/src/crash-reports.mjs +123 -0
  50. package/src/distill.mjs +30 -12
  51. package/src/escape.mjs +6 -4
  52. package/src/explore-distill.mjs +155 -0
  53. package/src/log.mjs +195 -0
  54. package/src/memory/code-sync.mjs +2 -1
  55. package/src/memory/core.mjs +11 -72
  56. package/src/memory/delete.mjs +234 -0
  57. package/src/memory/docs.mjs +206 -87
  58. package/src/memory.mjs +3 -1
  59. package/src/model-specs.mjs +15 -1
  60. package/src/peer-domains.mjs +265 -0
  61. package/src/peer-instances.mjs +231 -0
  62. package/src/prompt-overlays.mjs +25 -0
  63. package/src/prompts/advisor-design.md +18 -39
  64. package/src/prompts/advisor-round1.md +20 -32
  65. package/src/prompts/advisor-round2.md +16 -16
  66. package/src/prompts/advisor-round3.md +16 -16
  67. package/src/prompts/coder.md +7 -28
  68. package/src/prompts/consult-base.md +4 -11
  69. package/src/prompts/discipline.md +31 -44
  70. package/src/prompts/eng-coder.md +9 -34
  71. package/src/prompts/engineering-sub.md +10 -8
  72. package/src/prompts/engineering.md +61 -264
  73. package/src/prompts/explore.md +4 -14
  74. package/src/prompts/main.md +18 -35
  75. package/src/prompts/methodology-template.md +32 -38
  76. package/src/prompts/plan.md +2 -9
  77. package/src/prompts/system.md +18 -35
  78. package/src/provider/core.mjs +62 -69
  79. package/src/provider/errors.mjs +76 -0
  80. package/src/provider/retry.mjs +8 -45
  81. package/src/session-gc.mjs +214 -0
  82. package/src/session-guard.mjs +47 -0
  83. package/src/session-rename.mjs +38 -0
  84. package/src/session-slots.mjs +181 -58
  85. package/src/session.mjs +48 -89
  86. package/src/token-ttl.mjs +273 -0
  87. package/src/tools/apply_patch.md +3 -1
  88. package/src/tools/bash.md +1 -1
  89. package/src/tools/checklist-sync.mjs +181 -0
  90. package/src/tools/checklist.mjs +52 -39
  91. package/src/tools/delete.md +1 -0
  92. package/src/tools/edit-batch.mjs +131 -44
  93. package/src/tools/edit-diff.mjs +348 -0
  94. package/src/tools/edit.md +20 -13
  95. package/src/tools/execute.md +7 -7
  96. package/src/tools/execute.mjs +55 -24
  97. package/src/tools/file.mjs +25 -70
  98. package/src/tools/file_ops.md +2 -1
  99. package/src/tools/get_current_time.md +3 -1
  100. package/src/tools/git.mjs +14 -6
  101. package/src/tools/glob-dialect.mjs +130 -0
  102. package/src/tools/glob.md +3 -3
  103. package/src/tools/grep.md +1 -1
  104. package/src/tools/hashline_edit.md +2 -0
  105. package/src/tools/index.mjs +3 -3
  106. package/src/tools/insert_after.md +2 -1
  107. package/src/tools/lint.md +2 -0
  108. package/src/tools/lsp.md +4 -1
  109. package/src/tools/ops.mjs +175 -3
  110. package/src/tools/patch.mjs +84 -13
  111. package/src/tools/question.md +5 -1
  112. package/src/tools/repomap.mjs +1 -1
  113. package/src/tools/shared.mjs +18 -25
  114. package/src/tools/system.mjs +50 -30
  115. package/src/tools/tree.md +2 -1
  116. package/src/tools/wait_for.md +22 -0
  117. package/src/tools/web.mjs +5 -3
  118. package/src/tools/websearch.md +2 -1
  119. package/src/tools/write.md +2 -0
  120. package/src/traces/trace-store.mjs +224 -0
  121. package/src/tui/agent-turn.mjs +179 -27
  122. package/src/tui/clipboard.mjs +15 -4
  123. package/src/tui/cmd-config.mjs +77 -16
  124. package/src/tui/cmd-eng.mjs +20 -16
  125. package/src/tui/cmd-extract.mjs +1 -1
  126. package/src/tui/cmd-mcp.mjs +17 -2
  127. package/src/tui/cmd-new.mjs +3 -2
  128. package/src/tui/cmd-session.mjs +19 -4
  129. package/src/tui/cmd-think.mjs +11 -11
  130. package/src/tui/cmd-upgrade.mjs +19 -4
  131. package/src/tui/config-helpers.mjs +28 -16
  132. package/src/tui/distill-cmd.mjs +1 -1
  133. package/src/tui/index.mjs +31 -96
  134. package/src/tui/interaction.mjs +13 -2
  135. package/src/tui/key-handler.mjs +105 -155
  136. package/src/tui/key-modes.mjs +215 -0
  137. package/src/tui/layout.mjs +22 -1
  138. package/src/tui/mouse.mjs +46 -0
  139. package/src/tui/pickers.mjs +51 -25
  140. package/src/tui/render-conversation.mjs +13 -161
  141. package/src/tui/render-frame.mjs +27 -10
  142. package/src/tui/render-loop.mjs +4 -1
  143. package/src/tui/render-segments.mjs +182 -0
  144. package/src/tui/startup.mjs +40 -0
  145. package/src/tui/subagent-blocks.mjs +272 -262
  146. package/src/tui/subagent-children.mjs +176 -0
  147. package/src/tui/subagent-freeze.mjs +172 -0
  148. package/src/tui/subagent-panel.mjs +125 -12
  149. package/src/tui/suspension-drive.mjs +351 -0
  150. package/src/tui/tool-args.mjs +10 -2
  151. package/src/tui/tool-display.mjs +142 -0
  152. package/src/tui/tool-events.mjs +127 -231
  153. package/src/tui/tui-lifecycle.mjs +29 -0
  154. package/src/tui/update-notice.mjs +76 -0
  155. package/src/tui/wizard.mjs +48 -12
  156. package/src/agent-tools/escalate.mjs +0 -179
  157. package/src/agent-tools/subagent-check.mjs +0 -107
  158. package/src/tools/exec-prelude.mjs +0 -84
package/src/agent.mjs CHANGED
@@ -3,10 +3,10 @@
3
3
  * LLM ↔ tool-call loop, until the task is done.
4
4
  */
5
5
  import { chat } from "./provider/index.mjs"
6
- import { compressIfNeeded, compressFallback, COMPRESS_FAILURE_LIMIT, pushReal, summarizeRunExplorations } from "./context.mjs"
6
+ import { pushReal, summarizeRunExplorations } from "./context.mjs"
7
7
  import { specForModel } from "./config.mjs"
8
8
  import { readFileSync } from "node:fs"
9
- import { join, dirname } from "node:path"
9
+ import { join, dirname, resolve } from "node:path"
10
10
  import { fileURLToPath } from "node:url"
11
11
  import { executeToolCalls } from "./agent/dispatch.mjs"
12
12
  import { recordToolResults } from "./agent/record-results.mjs"
@@ -14,31 +14,28 @@ import { FILE_MUTATORS } from "./agent/helpers.mjs"
14
14
  import { prepareRun } from "./agent/setup.mjs"
15
15
  import { injectPostTurn } from "./agent/post-turn.mjs"
16
16
  import { handleCompletion } from "./agent/completion.mjs"
17
- import { cleanupConsultSessions } from "./agent-tools/consult.mjs"
17
+ // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
18
+ import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
18
19
  import {
19
- escapeXml, repairHistory, listWorkDir, ensureAutoReminder,
20
+ escapeXml, repairHistory, listWorkDir,
20
21
  readonlyToolNames, collectGitContext, loadProjectInstructions,
21
- ContinueError, offloadToolResult,
22
+ ContinueError,
22
23
  DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
23
24
  MIN_REPORT_CHARS, REPORT_CONTINUATION,
25
+ AUTO_TURN_DIGEST_DOMAIN,
24
26
  } from "./agent/helpers.mjs"
27
+ // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
28
+ // overlay 载荷(explore/coder/plan/eng-coder/consult)迁 prompt-overlays.mjs——re-export 保面
29
+ export {
30
+ EXPLORE_OVERLAY, CODER_OVERLAY, PLAN_OVERLAY, ENG_CODER_OVERLAY, CONSULT_BASE,
31
+ } from "./prompt-overlays.mjs"
32
+ export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
25
33
 
26
34
  // Prompt files (byte-stable, loaded once)
27
35
  const __dirname = dirname(fileURLToPath(import.meta.url))
28
36
  const SYSTEM_PROMPT = readFileSync(join(__dirname, "prompts", "system.md"), "utf8")
29
37
  const DISCIPLINE_RULES = readFileSync(join(__dirname, "prompts", "discipline.md"), "utf8")
30
38
  const MAIN_OVERLAY = readFileSync(join(__dirname, "prompts", "main.md"), "utf8")
31
- let _EXPLORE, _CODER, _PLAN, _ENG_CODER, _CONSULT_BASE
32
- try { _EXPLORE = readFileSync(join(__dirname, "prompts", "explore.md"), "utf8") } catch { _EXPLORE = "" }
33
- try { _CODER = readFileSync(join(__dirname, "prompts", "coder.md"), "utf8") } catch { _CODER = "" }
34
- try { _PLAN = readFileSync(join(__dirname, "prompts", "plan.md"), "utf8") } catch { _PLAN = "" }
35
- try { _ENG_CODER = readFileSync(join(__dirname, "prompts", "eng-coder.md"), "utf8") } catch { _ENG_CODER = "" }
36
- try { _CONSULT_BASE = readFileSync(join(__dirname, "prompts", "consult-base.md"), "utf8") } catch { _CONSULT_BASE = "" }
37
- export const EXPLORE_OVERLAY = _EXPLORE
38
- export const CODER_OVERLAY = _CODER
39
- export const PLAN_OVERLAY = _PLAN
40
- export const ENG_CODER_OVERLAY = _ENG_CODER
41
- export const CONSULT_BASE = _CONSULT_BASE
42
39
 
43
40
  // exported for consumption by agent-tools.mjs
44
41
  export {
@@ -49,34 +46,9 @@ export {
49
46
  }
50
47
 
51
48
 
52
- // Engineering mode reminder — shared with eng.mjs tool
53
- export const ENG_ON_REMINDER =
54
- "[System reminder: engineering mode is ON — design-before-code enforced. " +
55
- "Workflow: Requirements doc → Design doc → advisor(type='design') → " +
56
- "user approval → eng-coder implementation. Code changes go through eng-coder " +
57
- "subagents only. Advisor calls are NOT per-turn-mandatory — call only at " +
58
- "flow nodes or when the user asks.]"
59
-
60
49
  // Re-exported for API compatibility (single source of truth: advisor/repos.mjs)
61
50
  export { hasCodeMutations } from "./advisor/repos.mjs"
62
51
 
63
- /** Engineering mode OFF reminder — shared with the eng tool and the injector. */
64
- export const ENG_OFF_REMINDER =
65
- "[System reminder: engineering mode is now OFF — standard discipline applies. " +
66
- "Changes go through the normal workflow: you may edit files directly, advisor/verify " +
67
- "guards apply per config.]"
68
-
69
- /** Engineering-mode status injection — one reminder on EVERY transition (2026-08-25:
70
- * OFF is announced too — the model must know the gates lifted; silence after /eng-off
71
- * left it guessing. Covers TUI /eng, resume, and any path bypassing the eng tool.) */
72
- function injectEngineeringReminder(agent) {
73
- const eng = agent.config?.agent?.engineering ?? false
74
- if (eng !== agent._lastEngState) {
75
- agent.history.push({ role: "user", content: eng ? ENG_ON_REMINDER : ENG_OFF_REMINDER, transient: true })
76
- }
77
- agent._lastEngState = eng
78
- }
79
-
80
52
  /** Create a new agent state object with all fields initialized to defaults */
81
53
  export function createAgent({
82
54
  provider, tools, config, cwd, memory, overlay, role,
@@ -90,8 +62,12 @@ export function createAgent({
90
62
  planMode, autoApprove, goal,
91
63
  _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
92
64
  _engDesignReviewed: false, // eng-coder: design review gate passed (hard gate in dispatch.mjs)
93
- _engDesignToken: null, // issued by advisor(type="design"); required to spawn eng-coder
65
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): single-value `_engDesignToken` mirror retired
66
+ // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
67
+ // authoritative ledger (hydrated by restoreEngTokens / written by settle).
94
68
  _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
69
+ _advisorRuns: new Map(), // §24 D-24b: per-review convergence instances (rounds/prior/designId)
70
+ _mutationSeq: 0, _mutLog: [], // §24 D-24b: mutation log (in-flight review staleness scan)
95
71
  _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
96
72
  _lastEngState: false,
97
73
  _pendingReminders: [],
@@ -107,62 +83,90 @@ export function createAgent({
107
83
  }
108
84
 
109
85
  /** Run the agent loop: LLM ↔ tool-call cycle until task completion or turn limit. Returns final text content. */
110
- export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false } = {}) {
111
- // Previous run's async exploration distillation must settle before this run pushes new
112
- // input (SEND-STALL-DISTILL §2.2, N1): the compressed machine line is this run's starting
113
- // point — await BEFORE prepareRun, or the history replacement would wipe the new input.
86
+ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, suspDriven = false, consumeInjected = null } = {}) {
87
+ // Previous run's async exploration distillation must settle before this run pushes
88
+ // input (SEND-STALL-DISTILL §2.2 N1) await first, or its history replace wipes it.
114
89
  if (agent._pendingDistill) {
115
90
  const p = agent._pendingDistill
116
91
  agent._pendingDistill = null
117
92
  await p
118
93
  }
94
+ // §17 D-S3: suspension-settled async results inject before EVERY run's prepareRun
95
+ // (user + auto-turn); spliced = consumed. collectSettledAsync owns a different
96
+ // container, so no double-inject across the two consumption points.
97
+ // ASYNC-RESULT-CONTAINER.md D2 (2026-09-08):pending 单容器 `_pendingAsyncResults`
98
+ // +role——四族(subagent/advisor/escalate/consult)统一停靠;注入器按 role 分发
99
+ // (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 §25 D-R17a/b)
100
+ // ——单容器一处清,不再逐族三段。
101
+ const pendingAsync = agent._pendingAsyncResults
102
+ if (pendingAsync?.length) {
103
+ const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
104
+ const { injectConsultResult } = await import("./agent-tools/consult.mjs")
105
+ for (const e of pendingAsync.splice(0)) {
106
+ if (e.role === "consult") await injectConsultResult(agent, e)
107
+ else await injectAsyncResult(agent, e)
108
+ }
109
+ }
110
+ agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3)
119
111
  const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
120
112
  agent, input, callbacks,
121
- { depth, signal, overrideTurns, resume, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
113
+ { depth, signal, overrideTurns, resume: resume || autoTurn, systemPrompt: SYSTEM_PROMPT, disciplineRules: DISCIPLINE_RULES, mainOverlay: MAIN_OVERLAY },
122
114
  )
123
115
 
124
- // End-of-run exploration distillation boundary (CONTEXT-COMPACTION §5): prepareRun has already
125
- // pushed the user input + injections, so everything appended from here is "this run's" work.
116
+ // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
117
+ // pushed input + injections appended from here counts as "this run's" work.
126
118
  agent._runStartHistoryLen = agent.history.length
127
119
 
128
- // Per-run bookkeeping reset. On `resume` (ContinueError continuation) these are
129
- // PRESERVED: the resumed run must keep mutation tracking so the advisor/verify
130
- // guards stay active (a guard pushback on the last turn must not silently vanish),
131
- // and the convergence budget must not be resettable by continuing the session.
120
+ // Per-run bookkeeping reset PRESERVED on `resume` (ContinueError continuation):
121
+ // mutation/guard continuity and the convergence budget must survive a continuation.
132
122
  if (!resume) {
133
- agent._mutatedThisRun = false
134
- agent._verifiedThisRun = false
135
- agent._verifyPassed = undefined
136
- agent._calledAdvisorThisRun = false
137
- agent._touchedFiles = []
138
- agent._verifyRetries = 0
139
- agent._advisorRound = 0
140
- agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
141
- agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
142
- agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
143
- agent._asyncCheckLastN = 0 // subagent_check read counter is per-run (§15 D-A2): a fresh user turn restarts from 1
123
+ // §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
124
+ // reset) so auto-turn changes never escape the guard silently.
125
+ const g = agent._inheritedGuard
126
+ if (g) {
127
+ for (const k of ["_mutatedThisRun", "_verifiedThisRun", "_verifyPassed", "_calledAdvisorThisRun", "_touchedFiles", "_verifyRetries", "_advisorRound"]) agent[k] = g[k]
128
+ agent._inheritedGuard = null
129
+ } else {
130
+ agent._mutatedThisRun = false
131
+ agent._verifiedThisRun = false
132
+ agent._verifyPassed = undefined
133
+ agent._calledAdvisorThisRun = false
134
+ agent._touchedFiles = []
135
+ agent._verifyRetries = 0
136
+ agent._advisorRound = 0
137
+ agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
138
+ agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
139
+ agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
140
+ }
141
+ }
142
+ // §17 D-S6 manual tier: digest action-domain reminder (system-driven turn — organize only).
143
+ if (autoTurn && !agent.autoApprove) {
144
+ agent.history.push({ role: "user", content: AUTO_TURN_DIGEST_DOMAIN, transient: true })
144
145
  }
145
- // eng-coder authorization is set by subagent.mjs AFTER token validation but BEFORE runAgent —
146
- // only reset for the top-level agent (depth 0); child runs must keep their granted authorization
147
- if (depth === 0) agent._engDesignReviewed = false
148
- // _engDesignToken survives across turns within the same agent (design review → user approval → spawn eng-coder).
149
- // Lifecycle: invalidated on a failed re-review (advisor.mjs), issued on a passing review.
146
+ // eng-coder authorization (_engDesignReviewed) is eng-coder-only: set by subagent-spawn.mjs
147
+ // (spawn gate) / design-token.mjs (design review pass) BEFORE the child runAgent the
148
+ // depth-0 parent never reads or writes it (the parent gate reads anyLiveDesignSlot; the
149
+ // depth-0 per-turn reset was removed 2026-09-08, ENG-SESSION-PROVIDER-CLEANUP D1.3).
150
+ // Design slots (_engDesignTokens Map) survive across turns (design review approval
151
+ // eng-coder spawn) — persisted to the session slot at settle time (DESIGN-TOKEN-
152
+ // SETTLEMENT D1); lifecycle: issued on a passing review, consumed by consume-design / TTL.
150
153
  let guardPushbacks = 0
151
154
  let advisorPushbacks = 0
152
155
  let honestReminderInjected = false
153
156
  const recentCallSigs = []
154
- // repeat: "once" stream rules fire at most once per runAgent call (user turn):
155
- // this set survives across chat() calls (rule abort-retry, tool loop) within the turn.
157
+ // "once" stream rules fire at most once per runAgent call; the set survives across
158
+ // chat() calls (rule abort-retry, tool loop) within the turn.
156
159
  const streamRuleFired = new Set()
157
160
 
158
161
  // Compaction overhead for the pure-estimation path: system prompt + tools schema are
159
- // part of every request but not in history — without them the first-turn/restored/just-
160
- // compacted estimate under-counts and may never trigger compaction. Measured baseline
161
- // path already includes both (prompt_tokens is the full context), so this only applies
162
- // when _lastPromptTokens is null.
162
+ // in every request but not in history — without them the first-turn/just-compacted
163
+ // estimate under-counts and may never trigger. Measured path already includes both.
163
164
  const compactionOverhead = {
164
165
  systemPrompt,
165
166
  tools: toolSchemas,
167
+ // §18.6 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
168
+ // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
169
+ traceDepth: depth,
166
170
  }
167
171
 
168
172
  let thrownError = null
@@ -171,74 +175,33 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
171
175
  // Update turn counter for status bar display
172
176
  agent._currentTurn = turn + 1
173
177
  agent._maxTurns = maxTurns
174
- // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token on every
175
- // turn — a single emit point covering all three spawn tools (natural heartbeat for the
176
- // TUI subagent block header: "turn N/max"). phase=llm (tool/done progress rides the
177
- // existing onToolCall/onToolResult prefix relay — no token for those).
178
+ // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
179
+ // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
180
+ // the onToolCall/onToolResult relay no token for those).
178
181
  if (depth > 0 && callbacks.onToken) {
179
182
  callbacks.onToken(`⟦ev⟧turn\x1e${turn + 1}\x1e${maxTurns}\x1ellm\x1e`)
180
183
  }
181
184
 
185
+ // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
186
+ // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
187
+ // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
188
+ // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
189
+ consumeInjected?.(agent)
190
+
182
191
  const lastRole = agent.history.at(-1)?.role
183
192
  if (lastRole === "user" || lastRole === "tool") {
184
- try {
185
- if (await compressIfNeeded(agent, threshold, callbacks, compactionOverhead, signal)) {
186
- agent._compressFailures = 0
187
- agent._planReminderAtLen = 0 // After compression history shrinks, reset cadence so reminders resume
188
- recentCallSigs.length = 0 // After compression history is rebuilt, reset stall detection counter
189
- // Completion info (CONTEXT-COMPACTION §7 D-C2): { mode: "summary", tokensFreed, elapsedMs }
190
- // from compressIfNeeded, or { mode: "fallback", tailMessages } from compressFallback below —
191
- // the TUI panel renders the matching completion state. Existing callers that ignore the
192
- // argument keep the exact previous onCompress semantics.
193
- callbacks.onCompress?.(agent._lastCompressInfo ?? {})
194
- ensureAutoReminder(agent)
195
- }
196
- } catch (compressError) {
197
- // AbortError must not be swallowed: user cancellation must propagate
198
- if (compressError?.name === "AbortError" || signal?.aborted) throw compressError
199
- agent._compressFailures = (agent._compressFailures ?? 0) + 1
200
- // Q3 visibility (CONTEXT-COMPACTION §7 D-C1): a failed compression is no longer silent —
201
- // the frontend updates the compression panel with the error text (and logs to stderr).
202
- // Failure STRATEGY is unchanged: COMPRESS_FAILURE_LIMIT consecutive failures still degrade
203
- // to compressFallback — this only adds observability.
204
- callbacks?.onCompressFail?.(compressError)
205
- if (agent._compressFailures >= COMPRESS_FAILURE_LIMIT) {
206
- agent._compressFailures = 0
207
- if (compressFallback(agent)) callbacks.onCompress?.(agent._lastCompressInfo ?? {})
208
- }
209
- }
193
+ // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
194
+ // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
195
+ await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
210
196
  }
211
197
 
212
- // Plan-mode reminder cadence: re-inject constraint reminders while plan mode is active
213
- // (sparse every 2 turns, full every 5 turns or when the user sends a new message),
214
- // so the read-only restriction never fades from context.
215
- if (agent.planMode) {
216
- const lastMsg = agent.history.at(-1)
217
- const realUserMsg = lastMsg?.role === "user"
218
- && typeof lastMsg.content === "string"
219
- && !lastMsg.content.startsWith("[System reminder:")
220
- && !lastMsg.content.startsWith("[User interrupt:")
221
- const newUserSince = realUserMsg && agent.history.length > (agent._planReminderAtLen ?? 0)
222
- const { planReminderForTurn } = await import("./agent-tools/plan.mjs")
223
- const reminder = planReminderForTurn(agent, newUserSince)
224
- if (reminder) {
225
- agent._planReminderAtLen = agent.history.length + 1
226
- agent.history.push({ role: "user", content: reminder, transient: true })
227
- }
228
- }
229
-
230
- // Engineering-mode status injection: every new user message carries a
231
- // reminder so the model always knows whether it's in design-before-code
232
- // mode or standard discipline mode.
233
- if (depth === 0) {
234
- injectEngineeringReminder(agent)
235
- }
198
+ // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
199
+ await injectTurnReminders(agent, { depth })
236
200
 
237
201
  const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
238
202
  let response
239
203
 
240
- // Auto-think: classify task difficulty and set reasoning effort before the real prompt.
241
- // Runs only on turn 0 of user input; failure is silent — falls back to current setting.
204
+ // Auto-think: classify difficulty and set reasoning effort on turn 0; silent on failure.
242
205
  if (agent.config?.agent?.autoThink && turn === 0) {
243
206
  const { classifyAndApply } = await import("./auto-think.mjs")
244
207
  await classifyAndApply(agent, turn).catch(() => {})
@@ -253,14 +216,29 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
253
216
  signal,
254
217
  streamRules: agent.config.agent?.streamRules ?? [],
255
218
  firedPatterns: streamRuleFired,
219
+ // LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
220
+ // digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
221
+ // §18.6 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
222
+ // logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
223
+ // 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
224
+ // session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
225
+ logCtx: {
226
+ stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
227
+ role: agent._role ?? null,
228
+ depth,
229
+ kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
230
+ session: agent._sessionStart ?? null,
231
+ cwd: agent.cwd,
232
+ traces: agent.config?.traces?.enabled !== false,
233
+ },
256
234
  })
257
235
  } catch (e) {
258
- // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message: "…" }).
259
- // Inject the message into history and let the outer loop recreate the controller.
236
+ // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
237
+ // Inject into history; the outer loop recreates the controller and resumes.
260
238
  if (e.name === "AbortError" && signal?.reason?.interrupt) {
261
239
  const msg = `[User interrupt: ${signal.reason.message}]`
262
- // Dedup: if the interrupt was already handled during tool execution (L302-310),
263
- // don't push a duplicate — the outer loop will still recreate the controller.
240
+ // Dedup: if already handled during tool execution (interrupt branch below),
241
+ // don't push a duplicate — the outer loop still recreates the controller.
264
242
  if (agent.history.at(-1)?.content !== msg) {
265
243
  agent.history.push({ role: "user", content: msg })
266
244
  }
@@ -268,11 +246,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
268
246
  throw e
269
247
  }
270
248
 
271
- // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息,
272
- // 模型下一轮可见;全量回传时 transport tool_call_id 前缀还原 web_search_call item。
273
- // 注意:服务端 item id msg_xxx web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
274
- // 原始 id 存入 content(真机冒烟 2026-08-31:直接用 msg_xxx 会被转成 function_call_output
275
- // 与服务端不配对,属蒙对)。
249
+ // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
250
+ // 服务端 item id msg_xxx web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
251
+ // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
276
252
  for (const btr of response.builtinToolResults ?? []) {
277
253
  if (!btr?.id) continue
278
254
  pushReal(agent, {
@@ -282,8 +258,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
282
258
  })
283
259
  }
284
260
 
285
- // Stream rule triggered mid-generation (action: "abort"): halt current output,
286
- // inject rule's message as a reminder, and retry from the same context.
261
+ // Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
262
+ // message as a reminder, retry from the same context.
287
263
  if (response.ruleTriggered) {
288
264
  if (response.content) {
289
265
  pushReal(agent, { role: "assistant", content: response.content })
@@ -296,20 +272,12 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
296
272
  continue
297
273
  }
298
274
 
299
- // Stream rule warnings (action: "warn"): the stream completed, but one or more
300
- // non-interrupting rules matched. Inject warnings after the turn so the model
301
- // sees them before its next response — without aborting mid-generation.
302
- if (response._warnings?.length) {
303
- const deDuplicated = [...new Map(response._warnings.map(w => [w.name || w.pattern, w])).values()]
304
- agent.history.push({
305
- role: "user",
306
- content: `[System reminder — stream rule warnings from your last response:\n${deDuplicated.map(w => `- ${w.name || w.pattern}: ${w.message}`).join("\n")}]`,
307
- })
308
- }
275
+ // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
276
+ // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
277
+ injectResponseReminders(agent, response)
309
278
 
310
- // User interrupted mid-generation (Ctrl+I): the SSE stream was aborted while content
311
- // was partially generated. Commit partial output + inject user message, then signal
312
- // the outer loop to recreate the controller and resume.
279
+ // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
280
+ // message, then signal the outer loop to recreate the controller and resume.
313
281
  if (response.interrupted) {
314
282
  if (response.content) {
315
283
  pushReal(agent, { role: "assistant", content: response.content })
@@ -329,20 +297,8 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
329
297
  }
330
298
  }
331
299
 
332
- // Warn on abnormal finish reasons — the model stopped for a reason other than
333
- // "stop" or "tool_calls", meaning the response may be incomplete or truncated.
334
- if (response.finishReason && response.finishReason !== "stop" && response.finishReason !== "tool_calls") {
335
- const reasonMap = {
336
- length: "output token limit reached after exhausting continuations",
337
- insufficient_system_resource: "provider inference resources exhausted — consider retrying or switching models",
338
- content_filter: "response blocked by provider content filtering",
339
- }
340
- const detail = reasonMap[response.finishReason] || `unknown reason "${response.finishReason}"`
341
- agent.history.push({
342
- role: "user",
343
- content: `[System reminder: the previous turn ended abnormally — ${detail}. The assistant response that follows may be incomplete.]`,
344
- })
345
- }
300
+ // Warn on abnormal finish reasons — the response may be incomplete/truncated.
301
+ // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
346
302
 
347
303
  if (response.toolCalls.length === 0) {
348
304
  const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
@@ -351,13 +307,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
351
307
  advisorPushbacks = cr.advisorPushbacks
352
308
  if (cr.action === "continue") continue
353
309
  if (depth === 0) {
354
- // End-of-run exploration distillation (CONTEXT-COMPACTION §5): this run's inline
355
- // exploration results become one semantic note before the final return. Async
356
- // (SEND-STALL-DISTILL §2.1): the turn-end signal goes out first the promise hangs
357
- // on agent._pendingDistill and settles at the next runAgent's start or the TUI's
358
- // exit flush. Silent (N3): distillation failure must never block the return or lose
359
- // history.
360
- const distill = summarizeRunExplorations(agent, callbacks, signal).catch(() => {})
310
+ // End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
311
+ // §2.1): async the promise hangs on _pendingDistill, settling at the next run's
312
+ // start or the TUI exit flush. Silent (N3): failure never blocks return/history.
313
+ // §18.6 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道)
314
+ const distill = summarizeRunExplorations(agent, callbacks, signal, depth).catch(() => {})
361
315
  agent._pendingDistill = distill
362
316
  }
363
317
  return cr.content
@@ -380,12 +334,15 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
380
334
 
381
335
  const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
382
336
 
383
- // Ctrl+I interrupt during tool execution: skip committing partial results —
384
- // the tool failure messages would mislead the model. Inject the interrupt and retry.
337
+ // Ctrl+I interrupt during tool execution: skip committing partial results — inject
338
+ // the interrupt and retry (placeholder results keep strict providers pairable).
385
339
  if (signal?.reason?.interrupt) {
386
340
  // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
387
341
  // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
388
342
  // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
343
+ // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
344
+ // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
345
+ // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
389
346
  for (const { toolCall, ok } of results) {
390
347
  const tool = toolByName.get(toolCall.name)
391
348
  if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
@@ -397,16 +354,14 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
397
354
  const args = JSON.parse(toolCall.arguments)
398
355
  const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
399
356
  for (const p of paths) {
400
- const abs = join(agent.cwd, p)
357
+ const abs = resolve(agent.cwd, p)
401
358
  if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
402
359
  }
403
360
  } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
404
361
  }
405
- // The assistant tool_calls were already committed above (L347) a strict
406
- // provider 400s on dangling tool_calls, so synthesize placeholder tool
407
- // results BEFORE the interrupt message (tool result must immediately
408
- // follow its assistant tool_calls). The retry turn then sees a clean,
409
- // pairable history (consult P1, 2026-08-30).
362
+ // The assistant tool_calls were committed above — synthesize placeholder tool
363
+ // results BEFORE the interrupt message (strict providers 400 on dangling
364
+ // tool_calls; consult P1, 2026-08-30).
410
365
  for (const tc of response.toolCalls) {
411
366
  agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
412
367
  }
@@ -418,13 +373,11 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
418
373
  continue
419
374
  }
420
375
 
421
- // Model is executing tools → doing real work, reset guard pushback counter
376
+ // Model is executing tools → real work: reset guard pushback counters
422
377
  guardPushbacks = 0
423
378
  advisorPushbacks = 0
424
379
 
425
- // Commit tool results (pairing, multimodal deferral, mutation accounting,
426
- // touched files, reindex) — split into record-results.mjs (consult P2,
427
- // 2026-08-30).
380
+ // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
428
381
  await recordToolResults(agent, toolByName, results)
429
382
 
430
383
  injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
@@ -435,59 +388,9 @@ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal
435
388
  thrownError = e
436
389
  throw e
437
390
  } finally {
438
- // Turn-end cleanup: abort any leftover consultation children (consult_start spawns
439
- // fire-and-forget runners; a completed turn must not let them keep burning tokens).
440
- cleanupConsultSessions(agent)
441
- // Async subagent turn-end collection (AGENT-LOOP.md §15 D-A3). Lifecycle:
442
- // - Ctrl+C / Ctrl+I (signal aborted): children were aborted with the parent
443
- // signal — clear WITHOUT injecting stale errors (user explicitly stopped).
444
- // - ContinueError (turn cap): no wait, no injection — children keep running
445
- // and the RESUME run's turn-end collection takes over.
446
- // - anything else: refill loop → wait for all → inject reports → clear.
447
- if (signal?.aborted) {
448
- agent._asyncSubagents?.clear()
449
- agent._asyncQueue = []
450
- agent._asyncCheckLastN = 0
451
- } else if (thrownError instanceof ContinueError) {
452
- // keep _asyncSubagents + the check counter — the resumed run continues them
453
- } else {
454
- await collectAsyncSubagents(agent)
455
- agent._asyncCheckLastN = 0
456
- }
457
- }
458
- }
459
-
460
- /**
461
- * Turn-end async subagent collection (AGENT-LOOP.md §15 D-A3):
462
- * 1. refill loop — start queued heads while slots free (each settle already
463
- * refills via its finally; this drains the tail), keeping the cap ≤4 serial.
464
- * 2. wait for every entry to settle (queued entries start through the refill
465
- * chain — the drain loop converges when nothing is running and the queue is empty).
466
- * 3. inject one user-role reminder per entry: the report/error text XML-escaped
467
- * (child reports may carry content from files/webpages — reminder discipline),
468
- * >64K offloaded to disk with a preview + path.
469
- * 4. clear the map. (The ⟦ev⟧done freeze signal is NOT emitted here — D-A3
470
- * 2026-09-02: each entry's settle callback emits it at completion time so
471
- * blocks freeze at their completion position in the stream.)
472
- */
473
- async function collectAsyncSubagents(agent) {
474
- const map = agent._asyncSubagents
475
- if (!map || map.size === 0) return
476
- const { maybeRefillAsync } = await import("./agent-tools/subagent.mjs")
477
- for (;;) {
478
- maybeRefillAsync(agent)
479
- const running = [...map.values()].filter((e) => e.status === "running")
480
- if (running.length === 0) break
481
- await Promise.allSettled(running.map((e) => e.promise))
482
- }
483
- for (const e of [...map.values()]) {
484
- const body = e.error ?? e.report ?? "(no report)"
485
- const preview = await offloadToolResult(String(body), `async-subagent-${e.id}`)
486
- pushReal(agent, {
487
- role: "user",
488
- content: `[System reminder: async subagent #${e.id} (${e.role}) finished]\n${escapeXml(preview)}`,
489
- })
391
+ // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
392
+ // + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
393
+ // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
394
+ await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError })
490
395
  }
491
- map.clear()
492
- agent._asyncQueue = []
493
396
  }
@@ -81,6 +81,20 @@ export async function classifyAndApply(agent, turn) {
81
81
  ],
82
82
  tools: [],
83
83
  signal: AbortSignal.timeout(5_000),
84
+ // D-TS12 (AGENT-LOOP.md §18.7): full logCtx field set at the chat call
85
+ // point — traces/session/cwd/role/depth/kind (this call point carried
86
+ // only {stage,turn,child}). The traces field closes the D-TR6 "off = no
87
+ // persist" switch: without it the tracer treated the auto-think call as
88
+ // enabled and persisted even when agent.config.traces.enabled was false.
89
+ logCtx: {
90
+ stage: "autothink", turn, child: agent._logId,
91
+ traces: agent.config?.traces?.enabled !== false,
92
+ session: agent._sessionStart ?? null,
93
+ cwd: agent.cwd,
94
+ role: agent._role ?? null,
95
+ depth: agent._depth ?? 0, // agent state carries no depth stamp (the call site passes none) — 0 for the top-level agent
96
+ kind: "autothink",
97
+ },
84
98
  })
85
99
  const word = (response.content ?? "").trim().toLowerCase()
86
100
  if (word.startsWith("low")) level = "low"
@@ -10,7 +10,7 @@ function noKeyMessage() {
10
10
  return `还没有配置 API key。运行 thincoder 进入 TUI,用 /provider add 和 /provider key 配置;或直接编辑 ${configPath}`
11
11
  }
12
12
 
13
- /** thincoder distill <transcript-file> [--yes] [--scope=...]
13
+ /** thincoder distill <transcript-file> [--yes] [--layer=...]
14
14
  * Returns exit code: 0=success, 1=error */
15
15
  export async function distillCommand(args, exitSoon) {
16
16
  const flags = {}
@@ -20,9 +20,15 @@ export async function distillCommand(args, exitSoon) {
20
20
  if (m) flags[m[1]] = m[2] ?? true
21
21
  else positional.push(a)
22
22
  }
23
+ // AC2(MEMORY.md §6.5):显式拦截旧 flag——`--scope=X` 与 `--scope X` 两形态落入 flags.scope,
24
+ // 报错防静默吞参(解析器本不校验未知 flag——旧 --scope=project 静默落 personal 最危险)。
25
+ if (flags.scope !== undefined) {
26
+ console.error("distill: --scope renamed to --layer — update your invocation")
27
+ return 1
28
+ }
23
29
  const file = positional[0]
24
30
  if (!file) {
25
- console.error("Usage: thincoder distill <transcript-file> [--yes] [--scope=personal|project|team]")
31
+ console.error("Usage: thincoder distill <transcript-file> [--yes] [--layer=personal|project|team]")
26
32
  return 1
27
33
  }
28
34
  const { readFile } = await import("node:fs/promises")
@@ -64,9 +70,9 @@ export async function distillCommand(args, exitSoon) {
64
70
  }
65
71
  let saved = 0
66
72
  for (const c of candidates) {
67
- if (flags.scope) c.scope = flags.scope
73
+ if (flags.layer) c.layer = flags.layer
68
74
  console.log(`\n--- candidate ---`)
69
- console.log(`[${c.type}] ${c.title} (scope: ${c.scope})`)
75
+ console.log(`[${c.type}] ${c.title} (layer: ${c.layer})`)
70
76
  console.log(c.content)
71
77
  if (c.type === "rule") {
72
78
  console.log("(rule 类知识通常建议手动撰写;确认提取吗?)")