@thincoder/core 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/abort-provenance.mjs +116 -0
  2. package/advisor/citations.mjs +139 -0
  3. package/advisor/compaction.mjs +174 -0
  4. package/advisor/convergence.mjs +80 -0
  5. package/advisor/history.mjs +77 -0
  6. package/advisor/loop.mjs +288 -0
  7. package/advisor/messages.mjs +299 -0
  8. package/advisor/notice.mjs +141 -0
  9. package/advisor/project-context.mjs +197 -0
  10. package/advisor/repos.mjs +150 -0
  11. package/advisor/run.mjs +190 -0
  12. package/advisor/truncate.mjs +57 -0
  13. package/advisor.mjs +281 -0
  14. package/agent/child-marks.mjs +24 -0
  15. package/agent/completion.mjs +145 -0
  16. package/agent/dispatch.mjs +493 -0
  17. package/agent/family-tools.mjs +174 -0
  18. package/agent/helpers.mjs +412 -0
  19. package/agent/post-turn.mjs +70 -0
  20. package/agent/record-results.mjs +174 -0
  21. package/agent/relay-prefix.mjs +39 -0
  22. package/agent/run-stages.mjs +244 -0
  23. package/agent/setup-reminders.mjs +199 -0
  24. package/agent/setup.mjs +234 -0
  25. package/agent/spawn-child.mjs +258 -0
  26. package/agent/suspension.mjs +240 -0
  27. package/agent/write-gate.mjs +87 -0
  28. package/agent-tools/advisor-async.mjs +481 -0
  29. package/agent-tools/advisor-settle.mjs +240 -0
  30. package/agent-tools/advisor.mjs +280 -0
  31. package/agent-tools/async-discard.mjs +143 -0
  32. package/agent-tools/async-settle.mjs +299 -0
  33. package/agent-tools/batch-segment.mjs +265 -0
  34. package/agent-tools/child-permission.mjs +45 -0
  35. package/agent-tools/consult.mjs +471 -0
  36. package/agent-tools/design-token.mjs +117 -0
  37. package/agent-tools/digest-budget.mjs +76 -0
  38. package/agent-tools/eng.mjs +102 -0
  39. package/agent-tools/escalate-async.mjs +302 -0
  40. package/agent-tools/goal.mjs +119 -0
  41. package/agent-tools/panel-blocks.mjs +24 -0
  42. package/agent-tools/parent-channel.mjs +231 -0
  43. package/agent-tools/plan.mjs +86 -0
  44. package/agent-tools/read-history.mjs +309 -0
  45. package/agent-tools/recent-changes.mjs +24 -0
  46. package/agent-tools/review-facts.mjs +31 -0
  47. package/agent-tools/settings.mjs +268 -0
  48. package/agent-tools/skill.mjs +63 -0
  49. package/agent-tools/spawn-gates.mjs +109 -0
  50. package/agent-tools/subagent-actions.mjs +496 -0
  51. package/agent-tools/subagent-async.mjs +456 -0
  52. package/agent-tools/subagent-panel.mjs +160 -0
  53. package/agent-tools/subagent-run.mjs +208 -0
  54. package/agent-tools/subagent-scheduler.mjs +446 -0
  55. package/agent-tools/subagent-spawn.mjs +478 -0
  56. package/agent-tools/subagent.mjs +419 -0
  57. package/agent-tools/task.mjs +87 -0
  58. package/agent-tools/timer.mjs +46 -0
  59. package/agent-tools/verify.mjs +295 -0
  60. package/agent-tools.mjs +23 -0
  61. package/agent.mjs +430 -0
  62. package/auto-think.mjs +115 -0
  63. package/compress-form.mjs +24 -0
  64. package/config-io.mjs +277 -0
  65. package/config-migrate.mjs +178 -0
  66. package/config-presets.mjs +49 -0
  67. package/config.mjs +419 -0
  68. package/context.mjs +495 -0
  69. package/conventions.mjs +223 -0
  70. package/embedding.mjs +120 -0
  71. package/escape.mjs +152 -0
  72. package/expand-home.mjs +16 -0
  73. package/explore-distill.mjs +152 -0
  74. package/generate-title.mjs +123 -0
  75. package/git/checkpoint.mjs +448 -0
  76. package/git/gitmem.mjs +100 -0
  77. package/history-window.mjs +179 -0
  78. package/hooks.mjs +108 -0
  79. package/i18n.mjs +106 -0
  80. package/index-bin.mjs +48 -0
  81. package/index-discover.mjs +176 -0
  82. package/ledger-cmd.mjs +209 -0
  83. package/ledger-db.mjs +86 -0
  84. package/ledger-surface.mjs +76 -0
  85. package/ledger.mjs +202 -0
  86. package/log.mjs +195 -0
  87. package/manifest.mjs +338 -0
  88. package/markdown.mjs +106 -0
  89. package/mcp/helpers.mjs +51 -0
  90. package/mcp/transport-http.mjs +248 -0
  91. package/mcp/transport-stdio.mjs +140 -0
  92. package/mcp/transport-ws.mjs +122 -0
  93. package/mcp.mjs +295 -0
  94. package/memory/code-index.mjs +219 -0
  95. package/memory/code-sync.mjs +427 -0
  96. package/memory/core.mjs +318 -0
  97. package/memory/delete.mjs +242 -0
  98. package/memory/docs.mjs +431 -0
  99. package/memory/file-walk.mjs +109 -0
  100. package/memory/origin.mjs +24 -0
  101. package/memory/scan.mjs +177 -0
  102. package/memory/schema.mjs +460 -0
  103. package/memory.mjs +21 -0
  104. package/model-ref.mjs +66 -0
  105. package/model-specs.mjs +277 -0
  106. package/package.json +32 -0
  107. package/peer-domains.mjs +265 -0
  108. package/peer-instances.mjs +178 -0
  109. package/permission.mjs +79 -0
  110. package/process-probe.mjs +315 -0
  111. package/prompt-files.mjs +113 -0
  112. package/prompt-overlays.mjs +78 -0
  113. package/prompts/advisor-design.md +43 -0
  114. package/prompts/advisor-round1.md +41 -0
  115. package/prompts/advisor-round2.md +46 -0
  116. package/prompts/advisor-round3.md +42 -0
  117. package/prompts/common.md +158 -0
  118. package/prompts/consult-base.md +19 -0
  119. package/prompts/discipline-engineering.md +123 -0
  120. package/prompts/discipline-normal.md +206 -0
  121. package/prompts/persona-coder.md +21 -0
  122. package/prompts/persona-eng-coder.md +41 -0
  123. package/prompts/persona-eng-designer.md +80 -0
  124. package/prompts/persona-engineering.md +160 -0
  125. package/prompts/persona-explore.md +15 -0
  126. package/prompts/persona-normal.md +35 -0
  127. package/prompts/persona-plan.md +27 -0
  128. package/provider/anthropic.mjs +225 -0
  129. package/provider/core.mjs +491 -0
  130. package/provider/errors.mjs +101 -0
  131. package/provider/google.mjs +257 -0
  132. package/provider/index.mjs +7 -0
  133. package/provider/list-models.mjs +163 -0
  134. package/provider/normalize.mjs +81 -0
  135. package/provider/rate.mjs +168 -0
  136. package/provider/responses.mjs +495 -0
  137. package/provider/retry.mjs +88 -0
  138. package/provider/sse.mjs +264 -0
  139. package/provider/wait-status.mjs +59 -0
  140. package/proxy.mjs +274 -0
  141. package/rules.mjs +53 -0
  142. package/session-gc.mjs +248 -0
  143. package/session-guard.mjs +59 -0
  144. package/session-lifecycle.mjs +305 -0
  145. package/session-migrate.mjs +48 -0
  146. package/session-rename.mjs +38 -0
  147. package/session-segments.mjs +99 -0
  148. package/session-slot-write.mjs +168 -0
  149. package/session-slots-manifest.mjs +264 -0
  150. package/session-slots.mjs +298 -0
  151. package/session-store.mjs +441 -0
  152. package/session.mjs +244 -0
  153. package/skills.mjs +234 -0
  154. package/text-budget.mjs +79 -0
  155. package/token-ttl.mjs +285 -0
  156. package/tool-docs/apply_patch.md +15 -0
  157. package/tool-docs/bash.md +38 -0
  158. package/tool-docs/delete.md +13 -0
  159. package/tool-docs/edit.md +30 -0
  160. package/tool-docs/execute.md +21 -0
  161. package/tool-docs/fetch.md +12 -0
  162. package/tool-docs/file_ops.md +17 -0
  163. package/tool-docs/get_current_time.md +8 -0
  164. package/tool-docs/git.md +54 -0
  165. package/tool-docs/glob.md +11 -0
  166. package/tool-docs/grep.md +19 -0
  167. package/tool-docs/hashline_edit.md +14 -0
  168. package/tool-docs/insert_after.md +15 -0
  169. package/tool-docs/lint.md +10 -0
  170. package/tool-docs/ls.md +12 -0
  171. package/tool-docs/lsp.md +10 -0
  172. package/tool-docs/process.md +10 -0
  173. package/tool-docs/question.md +16 -0
  174. package/tool-docs/read.md +20 -0
  175. package/tool-docs/read_image.md +8 -0
  176. package/tool-docs/tree.md +14 -0
  177. package/tool-docs/wait_for.md +22 -0
  178. package/tool-docs/websearch.md +16 -0
  179. package/tool-docs/write.md +11 -0
  180. package/tools/bash.mjs +276 -0
  181. package/tools/edit-batch.mjs +204 -0
  182. package/tools/edit-diff.mjs +388 -0
  183. package/tools/exec-run.mjs +43 -0
  184. package/tools/execute.mjs +243 -0
  185. package/tools/file.mjs +464 -0
  186. package/tools/git-checkpoint.mjs +143 -0
  187. package/tools/git-ext.mjs +173 -0
  188. package/tools/git.mjs +415 -0
  189. package/tools/glob-dialect.mjs +130 -0
  190. package/tools/index.mjs +76 -0
  191. package/tools/linter.mjs +120 -0
  192. package/tools/lsp.mjs +335 -0
  193. package/tools/ops.mjs +293 -0
  194. package/tools/patch.mjs +290 -0
  195. package/tools/question.mjs +26 -0
  196. package/tools/repomap.mjs +314 -0
  197. package/tools/search.mjs +248 -0
  198. package/tools/shared.mjs +467 -0
  199. package/tools/tree.mjs +81 -0
  200. package/tools/web.mjs +224 -0
  201. package/tools/write-path.mjs +191 -0
  202. package/traces/trace-store.mjs +303 -0
  203. package/undo-stack.mjs +47 -0
package/agent.mjs ADDED
@@ -0,0 +1,430 @@
1
+ /**
2
+ * agent.mjs — Agent main loop
3
+ * LLM ↔ tool-call loop, until the task is done.
4
+ */
5
+ import { chat } from "./provider/index.mjs"
6
+ import { abortError, annotateAbort } from "./abort-provenance.mjs"
7
+ import { pushReal, summarizeRunExplorations } from "./context.mjs"
8
+ import { specForModel, assistantToolCallMessage } from "./config.mjs"
9
+ import { resolve } from "node:path"
10
+ import { executeToolCalls } from "./agent/dispatch.mjs"
11
+ import { recordToolResults } from "./agent/record-results.mjs"
12
+ import { FILE_MUTATORS } from "./agent/helpers.mjs"
13
+ import { prepareRun } from "./agent/setup.mjs"
14
+ import { injectPostTurn } from "./agent/post-turn.mjs"
15
+ import { handleCompletion } from "./agent/completion.mjs"
16
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
17
+ // (与子代理角色常量同源——单一权威锚 D1);旧三件文件读取随本批退役。
18
+ // 槽位常量通过下方 re-export 面(mod: rel — re-export 保 import 面);本文件自身零直接消费。
19
+ // 主循环阶段函数(压缩检查/注入组/回合收尾)2026-09-05 实践轮迁 agent/run-stages.mjs
20
+ import { runCompactionCheck, injectTurnReminders, finalizeAgentTurn, injectResponseReminders } from "./agent/run-stages.mjs"
21
+ import {
22
+ escapeXml, repairHistory, listWorkDir,
23
+ readonlyToolNames, collectGitContext, loadProjectInstructions,
24
+ ContinueError,
25
+ turnFrame, // 第 19 批(TURN-ACROSS-SEGMENTS):跨段累计编号帧(设计 §19.3)
26
+ DEFAULT_MAX_TURNS, DEFAULT_SUBAGENT_TURNS,
27
+ MIN_REPORT_CHARS, REPORT_CONTINUATION,
28
+ AUTO_TURN_DIGEST_DOMAIN,
29
+ UPSTREAM_TURN_DOMAIN, // §6.27.12.8:上行唤醒轮域文本(手动档——ask 轮不沿用 digest 域文本)
30
+ restoreGuard, // §17 D-S6 读侧单点(P2 机制层端差批 §2.18——键清单归核)
31
+ } from "./agent/helpers.mjs"
32
+ // ENG 提醒族 + auto-turn domain 2026-09-05 迁 agent/helpers.mjs(agent.mjs 530 > 500 硬限)
33
+ // PROMPT-SYSTEM 施工② G1(2026-09-10):六件槽位常量装载收口 prompt-overlays.mjs
34
+ // (与子代理角色常量同源——单一权威锚 D1);本文件 re-export 保 import 面。
35
+ export {
36
+ PERSONA_ENGINEERING, PERSONA_NORMAL, COMMON,
37
+ DISCIPLINE_ENGINEERING, DISCIPLINE_NORMAL,
38
+ CONSULT_BASE,
39
+ } from "./prompt-overlays.mjs"
40
+ export { ENG_ON_REMINDER, ENG_OFF_REMINDER } from "./agent/helpers.mjs"
41
+
42
+ // Role → persona slot mapping note(施工② G3): 子代理人格由 assemblePrompt 场景表按
43
+ // role 承载(persona-explore/coder/plan/eng-coder.md——D1 表 = 蓝图 §3.2 1:1);
44
+ // spawn 侧 overlay 概念已退役(prompt-overlays.mjs 不再导出 OVERLAY 别名常量)。
45
+
46
+ // exported for consumption by agent-tools.mjs
47
+ export {
48
+ ContinueError,
49
+ listWorkDir, loadProjectInstructions,
50
+ readonlyToolNames, collectGitContext, escapeXml,
51
+ MIN_REPORT_CHARS, REPORT_CONTINUATION, DEFAULT_SUBAGENT_TURNS,
52
+ }
53
+
54
+
55
+ // Re-exported for API compatibility (single source of truth: advisor/repos.mjs)
56
+ export { hasCodeMutations } from "./advisor/repos.mjs"
57
+
58
+ /** Create a new agent state object with all fields initialized to defaults */
59
+ export function createAgent({
60
+ provider, tools, config, cwd, memory, overlay, role,
61
+ tasks = [], history = [],
62
+ planMode = false, autoApprove = false,
63
+ goal = null, sessionStart = null,
64
+ }) {
65
+ return {
66
+ provider, tools, config, cwd, memory, _role: role,
67
+ overlay, tasks, history,
68
+ planMode, autoApprove, goal,
69
+ _mutatedThisRun: false, _verifiedThisRun: false, _verifyPassed: undefined, _calledAdvisorThisRun: false,
70
+ _engDesignReviewed: false, // eng-coder: design review gate passed (hard gate in dispatch.mjs)
71
+ // DESIGN-TOKEN-SETTLEMENT D3 (2026-09-08): single-value `_engDesignToken` mirror retired
72
+ // (AC3 零写) — no field initializer; the multi-slot Map `_engDesignTokens` is the
73
+ // authoritative ledger (hydrated by restoreEngTokens / written by settle).
74
+ _touchedFiles: [], _verifyRetries: 0, _advisorRound: 0, _advisorSession: null,
75
+ _advisorRuns: new Map(), // §11.2 D-24b: per-review convergence instances (rounds/prior/designId)
76
+ _mutationSeq: 0, _mutLog: [], // §11.2 D-24b: mutation log (in-flight review staleness scan)
77
+ _lastAdvisorOutput: null, // full review output from the most recent advisor call (convergence rounds inject it verbatim)
78
+ _lastEngState: false,
79
+ _pendingReminders: [],
80
+ _pendingTimers: [],
81
+ _sessionStart: sessionStart,
82
+ _lastPromptTokens: null, _usageAtLen: null,
83
+ _compressFailures: 0,
84
+ _emptyRetries: 0, // empty-response retry budget (per-run; reset on a fresh user turn)
85
+ _runStartHistoryLen: 0, // machine-line length at the start of the current run — end-of-run exploration distillation slices from here
86
+ _pendingDistill: null, // in-flight end-of-run exploration distillation (SEND-STALL-DISTILL §2.1) — awaited at next run start / TUI exit flush
87
+ _currentTurn: 0, _maxTurns: DEFAULT_MAX_TURNS, // turn counter for status bar display
88
+ }
89
+ }
90
+
91
+ /** 流式输出门(§2.5 #78 并入——VSC 三态门,纯函数可直驱测试):depth 0 恒通;
92
+ * consult 子代理豁免(其输出进会诊面板);其余经 `streamOutput` 显式选择进入。 */
93
+ export function streamOutputAllowed(depth, role, streamOutput = false) {
94
+ return depth === 0 || role === "consult" || streamOutput === true
95
+ }
96
+
97
+ /** Run the agent loop: LLM ↔ tool-call cycle until task completion or turn limit. Returns final text content. */
98
+ export async function runAgent(agent, input, callbacks = {}, { depth = 0, signal, maxTurns: overrideTurns, resume = false, autoTurn = false, upstreamTurn = false, suspDriven = false, consumeInjected = null, streamOutput = false, extraTools = null } = {}) {
99
+ // Previous run's async exploration distillation must settle before this run pushes
100
+ // input (SEND-STALL-DISTILL §2.2 N1) — await first, or its history replace wipes it.
101
+ if (agent._pendingDistill) {
102
+ const p = agent._pendingDistill
103
+ agent._pendingDistill = null
104
+ await p
105
+ }
106
+ // §17 D-S3: suspension-settled async results inject before EVERY run's prepareRun
107
+ // (user + auto-turn); spliced = consumed. collectSettledAsync owns a different
108
+ // container, so no double-inject across the two consumption points.
109
+ // ASYNC-RESULT-CONTAINER.md D2 (2026-09-08):pending 单容器 `_pendingAsyncResults`
110
+ // +role——四族(subagent/advisor/escalate/consult)统一停靠;注入器按 role 分发
111
+ // (consult → injectConsultResult;其余 → injectAsyncResult——族分支同 §25 D-R17a/b)
112
+ // ——单容器一处清,不再逐族三段。
113
+ const pendingAsync = agent._pendingAsyncResults
114
+ if (pendingAsync?.length) {
115
+ const { injectAsyncResult } = await import("./agent-tools/subagent.mjs")
116
+ const { injectConsultResult } = await import("./agent-tools/consult.mjs")
117
+ // TUI-OOM-ROOTCAUSE(AGENT-LOOP.md §23.3.1 消费点②——run 起始 pending 注入):
118
+ // 注入完成后释放条目对子代理对象的持有(childAgent/report 置空——幂等 helper)。
119
+ const { releaseSettledEntry } = await import("./agent-tools/async-settle.mjs")
120
+ for (const e of pendingAsync.splice(0)) {
121
+ if (e.role === "consult") await injectConsultResult(agent, e)
122
+ else await injectAsyncResult(agent, e)
123
+ releaseSettledEntry(e)
124
+ }
125
+ }
126
+ agent._inAutoTurn = autoTurn // spawn gate for manual-tier digests (§17 D-S6/N3;上行唤醒轮同持 autoTurn——§6.27.12.4 ①)
127
+ const { maxTurns, threshold, tools, toolSchemas, toolByName, systemPrompt } = await prepareRun(
128
+ // G1/G2(施工②):prompt 装配收口 prepareRun 内部(assemblePrompt——prompt-overlays.mjs
129
+ // 槽位常量,与子代理角色常量同源——单一权威锚 D1);本调用不再携带 prompt 常量。
130
+ agent, input, callbacks,
131
+ { depth, signal, overrideTurns, resume: resume || autoTurn, extraTools },
132
+ )
133
+
134
+ // Exploration-distillation boundary (CONTEXT-COMPACTION §5): prepareRun already
135
+ // pushed input + injections — appended from here counts as "this run's" work.
136
+ agent._runStartHistoryLen = agent.history.length
137
+
138
+ // Per-run bookkeeping reset — PRESERVED on `resume` (ContinueError continuation):
139
+ // mutation/guard continuity and the convergence budget must survive a continuation.
140
+ if (!resume) {
141
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):链内累计编号
142
+ // 只在链起点复位——续跑(resume:true)不重置、不回退(编号帧公式见 helpers.mjs
143
+ // turnFrame)。与下方 mutation/guard 复位同条件同点(全档唯一复位点)。
144
+ agent._turnSeq = 0
145
+ // §17 D-S6: an auto-turn's guard marks are inherited by the next USER run (not
146
+ // reset) so auto-turn changes never escape the guard silently.
147
+ const g = agent._inheritedGuard
148
+ if (g) {
149
+ restoreGuard(agent, g)
150
+ agent._inheritedGuard = null
151
+ } else {
152
+ agent._mutatedThisRun = false
153
+ agent._verifiedThisRun = false
154
+ agent._verifyPassed = undefined
155
+ agent._calledAdvisorThisRun = false
156
+ agent._touchedFiles = []
157
+ agent._verifyRetries = 0
158
+ agent._advisorRound = 0
159
+ agent._advisorSession = null // advisor session is per-run: discard when the task ends, next task starts fresh
160
+ agent._emptyRetries = 0 // empty-response retry budget is per-run: a fresh user turn restarts from zero
161
+ agent._compressFailures = 0 // compaction summary-failure counter is per-run: a fresh user turn restarts from zero
162
+ }
163
+ }
164
+ // §17 D-S6 manual tier: digest action-domain reminder (system-driven turn — organize only).
165
+ // §6.27.12.4 ②: an up-stream wake turn answers a RUNNING subagent waiting for the reply — it
166
+ // must not reuse the digest text ("no one is waiting" is the opposite of the truth).
167
+ if ((autoTurn || upstreamTurn) && !agent.autoApprove) {
168
+ agent.history.push({ role: "user", content: upstreamTurn ? UPSTREAM_TURN_DOMAIN : AUTO_TURN_DIGEST_DOMAIN, transient: true })
169
+ }
170
+ // eng-coder authorization (_engDesignReviewed) is eng-coder-only: set by subagent-spawn.mjs
171
+ // (spawn gate) / design-token.mjs (design review pass) BEFORE the child runAgent — the
172
+ // depth-0 parent never reads or writes it (the parent gate reads anyLiveDesignSlot; the
173
+ // depth-0 per-turn reset was removed 2026-09-08, ENG-SESSION-PROVIDER-CLEANUP D1.3).
174
+ // Design slots (_engDesignTokens Map) survive across turns (design review → approval →
175
+ // eng-coder spawn) — persisted to the session slot at settle time (DESIGN-TOKEN-
176
+ // SETTLEMENT D1); lifecycle: issued on a passing review, consumed by consume-design / TTL.
177
+ let guardPushbacks = 0
178
+ let advisorPushbacks = 0
179
+ let honestReminderInjected = false
180
+ const recentCallSigs = []
181
+ // "once" stream rules fire at most once per runAgent call; the set survives across
182
+ // chat() calls (rule abort-retry, tool loop) within the turn.
183
+ const streamRuleFired = new Set()
184
+
185
+ // Compaction overhead for the pure-estimation path: system prompt + tools schema are
186
+ // in every request but not in history — without them the first-turn/just-compacted
187
+ // estimate under-counts and may never trigger. Measured path already includes both.
188
+ const compactionOverhead = {
189
+ systemPrompt,
190
+ tools: toolSchemas,
191
+ // §18.6 D-TR4:compress 轨迹 depth 元数据(runAgent 的 depth 在此作用域——
192
+ // context.mjs compressIfNeeded 经 extras 透出到 logCtx)
193
+ traceDepth: depth,
194
+ }
195
+
196
+ // SUBAGENT-UPSTREAM-CHANNEL(AGENT-LOOP-SUBAGENT.md §6.27.4 消费点):子 → 父在飞消息的
197
+ // 回合边界注入单点取用一次(模块缓存 ⇒ 每 run 一次代价);动态 import = 零新增静态边
198
+ // (先例 = 上方 injectAsyncResult :113-117)。
199
+ const { drainChildUpstream } = await import("./agent-tools/parent-channel.mjs")
200
+
201
+ let thrownError = null
202
+ try {
203
+ for (let turn = 0; turn < maxTurns; turn++) {
204
+ // 第 19 批(TURN-ACROSS-SEGMENTS——设计 TURN-CAP-CONTINUE.md §19.3):编号帧——
205
+ // `_turnSeq` 每轮 +1(跨段累计,仅 `!resume` 链起点复位);面向消费面的两字段
206
+ // (状态行 + ⟦ev⟧turn / ⟦ev⟧approval 事件共用)在此同点赋值(编号唯一权威;帧
207
+ // 公式 = helpers.mjs turnFrame)。段内帽判定不读帧(下行循环条件只读段内
208
+ // turn / maxTurns——N6)。
209
+ const frame = turnFrame(++agent._turnSeq, turn, maxTurns)
210
+ agent._currentTurn = frame.turn
211
+ agent._maxTurns = frame.maxTurns
212
+ // D2 (AGENT-LOOP.md §7.2): depth>0 children emit a ⟦ev⟧turn progress token each turn —
213
+ // single emit point covering all three spawn tools; phase=llm (tool/done progress rides
214
+ // the onToolCall/onToolResult relay — no token for those). 第 19 批:载荷取上方帧值
215
+ // (agent._currentTurn / _maxTurns——字段形态 / 字段数 / phase 零变化,N5)。
216
+ if (depth > 0 && callbacks.onToken) {
217
+ callbacks.onToken(`⟦ev⟧turn\x1e${agent._currentTurn}\x1e${agent._maxTurns}\x1ellm\x1e`)
218
+ }
219
+ // §2.5 #78 并入(VSC 帧回调):每轮帧另经结构化回调发出(池条目 entry.turn 的
220
+ // VSC 等价通道——⟦ev⟧turn token 解析的宿主面随时可用;签名扩展向后兼容)。
221
+ callbacks.onAgentTurn?.(frame.turn, frame.maxTurns)
222
+
223
+ // SUBAGENT-OBSERVE-SEND D2: 子代理回合边界消费点——每轮开头把父侧经 subagent
224
+ // action:'send' 注入队列(entry._injected)的消息按普通 user 回合推入子历史
225
+ // (pushReal → 下一轮 chat 即含该指令)。由 executeAsyncSpawn 经 childRunOpts 贯通的
226
+ // consumeInjected 回调承载(异步子代理专属——缺省 null:主会话/阻塞子代理零开销)。
227
+ consumeInjected?.(agent)
228
+ // 子代理在飞消息(子 → 父;§6.27):空队列 no-op;非空 ⇒ 恰一条合并 user 消息注入。
229
+ drainChildUpstream(agent)
230
+
231
+ const lastRole = agent.history.at(-1)?.role
232
+ if (lastRole === "user" || lastRole === "tool") {
233
+ // 2026-09-05 实践轮:压缩检查/降级计数提为 runCompactionCheck(agent/run-stages.mjs——
234
+ // CLI 对位 VS run-stages)——循环骨架此处只剩检查调用(recentCallSigs 对象引用回流)。
235
+ await runCompactionCheck(agent, { threshold, callbacks, compactionOverhead, signal, recentCallSigs })
236
+ }
237
+
238
+ // 2026-09-05 实践轮:plan cadence + eng 状态注入提为 injectTurnReminders(run-stages)
239
+ await injectTurnReminders(agent, { depth })
240
+
241
+ const messages = [{ role: "system", content: systemPrompt }, ...agent.history]
242
+ let response
243
+
244
+ // Auto-think: classify difficulty and set reasoning effort on turn 0; silent on failure.
245
+ if (agent.config?.agent?.autoThink && turn === 0) {
246
+ const { classifyAndApply } = await import("./auto-think.mjs")
247
+ await classifyAndApply(agent, turn).catch(() => {})
248
+ }
249
+
250
+ if (process.env.ADVISOR_DEBUG) console.error("[chat-call]", JSON.stringify({ turn, histLen: agent.history.length, lastRole: agent.history.at(-1)?.role }))
251
+ try { response = await chat(agent.provider, {
252
+ messages, tools: toolSchemas,
253
+ // §2.5 #78 并入(VSC onToken 三态门):depth 0 恒通;consult 子代理豁免(其输出进
254
+ // 会诊面板);escalate 等经 opts.streamOutput 显式选择进入;其余子代理不流式。
255
+ onToken: streamOutputAllowed(depth, agent._role, streamOutput) ? callbacks.onToken : null,
256
+ onReasoning: callbacks.onReasoning,
257
+ onWait: callbacks.onWait,
258
+ signal,
259
+ streamRules: agent.config.agent?.streamRules ?? [],
260
+ firedPatterns: streamRuleFired,
261
+ // LOGGING(LOGGING.md):llm:* 事件的语义上下文(stage=turn 主循环回合——含
262
+ // digest 消化轮 auto=true;child=子代理 id(spawn 时 stamp 于 child._logId))
263
+ // §18.6 D-TR4:轨迹元数据增补(role/depth/kind/session/cwd——trace-store 只读
264
+ // logCtx,签名不变);kind:depth>0 = subagent(consult 孩子 = consult)——子代理
265
+ // 对回靠 role+depth+child id(children 无 _sessionStart——不经 depth-0 设置——
266
+ // session 字段对子代理轨迹为 null——见 trace-store/agent.mjs 注释)。
267
+ logCtx: {
268
+ stage: "turn", turn: turn + 1, auto: autoTurn, child: agent._logId,
269
+ role: agent._role ?? null,
270
+ depth,
271
+ kind: depth > 0 ? (agent._role === "consult" ? "consult" : "subagent") : "turn",
272
+ session: agent._sessionStart ?? null,
273
+ cwd: agent.cwd,
274
+ traces: agent.config?.traces?.enabled !== false,
275
+ },
276
+ })
277
+ } catch (e) {
278
+ // User interrupt (Ctrl+I): controller.abort({ interrupt: true, message }).
279
+ // Inject into history; the outer loop recreates the controller and resumes.
280
+ if (e.name === "AbortError" && signal?.reason?.interrupt) {
281
+ const msg = `[User interrupt: ${signal.reason.message}]`
282
+ // Dedup: if already handled during tool execution (interrupt branch below),
283
+ // don't push a duplicate — the outer loop still recreates the controller.
284
+ if (agent.history.at(-1)?.content !== msg) {
285
+ agent.history.push({ role: "user", content: msg })
286
+ }
287
+ }
288
+ throw e
289
+ }
290
+
291
+ // 内置工具(Responses web_search)结果本地化:服务端已执行——入历史为 tool 消息;
292
+ // 服务端 item id 是 msg_xxx 非 web_search_call_ 前缀——必须合成前缀(toItems 识别锚点),
293
+ // 原始 id 存入 content(真机冒烟 2026-08-31 验证)。
294
+ for (const btr of response.builtinToolResults ?? []) {
295
+ if (!btr?.id) continue
296
+ pushReal(agent, {
297
+ role: "tool",
298
+ tool_call_id: `web_search_call_${btr.id}`,
299
+ content: JSON.stringify({ id: btr.id, query: btr.query ?? "", sources: btr.sources ?? [], status: btr.status ?? "completed" }),
300
+ })
301
+ }
302
+
303
+ // Stream rule triggered mid-generation (action: "abort"): halt, inject the rule's
304
+ // message as a reminder, retry from the same context.
305
+ if (response.ruleTriggered) {
306
+ if (response.content) {
307
+ pushReal(agent, { role: "assistant", content: response.content })
308
+ }
309
+ const label = response.ruleName ? ` — stream rule "${response.ruleName}"` : ""
310
+ agent.history.push({
311
+ role: "user",
312
+ content: `[System reminder${label}: ${response.ruleMessage}]`,
313
+ })
314
+ continue
315
+ }
316
+
317
+ // Stream rule warnings / finish-reason 警告(2026-09-05 实践轮——提为
318
+ // injectResponseReminders,agent/run-stages.mjs——verbatim,语义零变)
319
+ injectResponseReminders(agent, response)
320
+
321
+ // User interrupted mid-generation (Ctrl+I): commit partial output + inject the
322
+ // message, then signal the outer loop to recreate the controller and resume.
323
+ if (response.interrupted) {
324
+ if (response.content) {
325
+ pushReal(agent, { role: "assistant", content: response.content })
326
+ }
327
+ agent.history.push({
328
+ role: "user",
329
+ content: `[User interrupt: ${response.interruptMessage}]`,
330
+ })
331
+ // §20.3 站点 #8(第 24 批):错误对象已自带 name/message——只补来源标注(缺 abortInfo 才补)
332
+ throw annotateAbort(Object.assign(new Error("User interrupted"), { name: "AbortError" }), signal, "agent", "interrupted-response")
333
+ }
334
+
335
+ if (response.usage) {
336
+ callbacks.onUsage?.(response.usage)
337
+ if (response.usage.prompt_tokens != null) {
338
+ agent._lastPromptTokens = response.usage.prompt_tokens
339
+ agent._usageAtLen = agent.history.length
340
+ }
341
+ }
342
+
343
+ // Warn on abnormal finish reasons — the response may be incomplete/truncated.
344
+ // 2026-09-05 实践轮:finish-reason 注入随流规则警告提为 injectResponseReminders。
345
+
346
+ if (response.toolCalls.length === 0) {
347
+ const cr = handleCompletion(agent, response, depth, turn, guardPushbacks, honestReminderInjected, advisorPushbacks, callbacks)
348
+ guardPushbacks = cr.guardPushbacks
349
+ honestReminderInjected = cr.honestReminderInjected
350
+ advisorPushbacks = cr.advisorPushbacks
351
+ if (cr.action === "continue") continue
352
+ if (depth === 0) {
353
+ // End-of-run exploration distillation (CONTEXT-COMPACTION §5 + SEND-STALL-DISTILL
354
+ // §2.1): async — the promise hangs on _pendingDistill, settling at the next run's
355
+ // start or the TUI exit flush. Silent (N3): failure never blocks return/history.
356
+ // §18.6 D-TR4:depth 透传(distill 轨迹元数据——与 compress 同通道);extras = 会话续写前缀面(§6.15)
357
+ const distill = summarizeRunExplorations(agent, callbacks, signal, depth,
358
+ { systemPrompt, tools: toolSchemas }).catch(() => {}) // 与回合请求同源(无第二构造点)
359
+ agent._pendingDistill = distill
360
+ }
361
+ return cr.content
362
+ }
363
+
364
+ // abort after chat completes, before committing history: don't commit a half-finished turn
365
+ if (signal?.aborted) throw abortError(signal, "agent", "post-chat")
366
+
367
+ pushReal(agent, assistantToolCallMessage(response, specForModel(agent.provider.model)))
368
+
369
+ const results = await executeToolCalls(agent, toolByName, response.toolCalls, callbacks, depth, signal)
370
+
371
+ // Ctrl+I interrupt during tool execution: skip committing partial results — inject
372
+ // the interrupt and retry (placeholder results keep strict providers pairable).
373
+ if (signal?.reason?.interrupt) {
374
+ // 中断变更记账(2026-08-31 评审 #4):此分支的工具已全部执行完成(磁盘已变,execute 已完成),
375
+ // 真实结果按语义不进历史(placeholder 替代)——但变更必须记账:否则 guard 看到
376
+ // "本轮未改代码" 放行,评审/verify 门禁被绕过(文件改了却没评审)。
377
+ // §29 fix A(2026-09-07):mutation-seq 记账已收敛到 dispatch runOne 执行成功即刻
378
+ // (唯一记账点——本分支不再 noteMutations——不双计——中断+同批 launch seq 单计
379
+ // 回归断言见 §29 T-A1i);此处仅剩 guard 标志 + touchedFiles 记账。
380
+ for (const { toolCall, ok } of results) {
381
+ const tool = toolByName.get(toolCall.name)
382
+ if (!ok || !tool || !FILE_MUTATORS.has(toolCall.name)) continue
383
+ agent._mutatedThisRun = true
384
+ agent._calledAdvisorThisRun = false
385
+ agent._verifiedThisRun = false
386
+ agent._verifyPassed = undefined
387
+ try {
388
+ const args = JSON.parse(toolCall.arguments)
389
+ const paths = tool.touchedPaths ? tool.touchedPaths(args) : [args.path]
390
+ for (const p of paths) {
391
+ const abs = resolve(agent.cwd, p)
392
+ if (!agent._touchedFiles.includes(abs)) agent._touchedFiles.push(abs)
393
+ }
394
+ } catch { /* 畸形 args 不影响记账(touchedFiles 尽力而为) */ }
395
+ }
396
+ // The assistant tool_calls were committed above — synthesize placeholder tool
397
+ // results BEFORE the interrupt message (strict providers 400 on dangling
398
+ // tool_calls; consult P1, 2026-08-30).
399
+ for (const tc of response.toolCalls) {
400
+ agent.history.push({ role: "tool", tool_call_id: tc.id, content: "[Tool execution interrupted — results discarded]" })
401
+ }
402
+ agent.history.push({
403
+ role: "user",
404
+ content: `[User interrupt: ${signal.reason.message}]`,
405
+ })
406
+ callbacks.onTurnEnd?.(agent, turn)
407
+ continue
408
+ }
409
+
410
+ // Model is executing tools → real work: reset guard pushback counters
411
+ guardPushbacks = 0
412
+ advisorPushbacks = 0
413
+
414
+ // Commit tool results (pairing, multimodal deferral, mutation accounting, reindex)
415
+ await recordToolResults(agent, toolByName, results)
416
+
417
+ injectPostTurn(agent, results, recentCallSigs, callbacks, turn)
418
+ }
419
+
420
+ throw new ContinueError(maxTurns)
421
+ } catch (e) {
422
+ thrownError = e
423
+ throw e
424
+ } finally {
425
+ // 2026-09-05 实践轮:回合收尾(consult 清理/async 池分流/guard 继承——原 425-462
426
+ // 段 + collectSettledAsync 466-494 整体迁 agent/run-stages.mjs finalizeAgentTurn——
427
+ // CLI 对位 VS run-stages——finally 只剩一行调用 + 骨架注释)。
428
+ await finalizeAgentTurn(agent, { signal, autoTurn, suspDriven, thrownError, depth })
429
+ }
430
+ }
package/auto-think.mjs ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * auto-think.mjs — automatic difficulty classification for reasoning effort.
3
+ *
4
+ * When enabled, before each user-facing turn a cheap classification call determines
5
+ * the task difficulty, then maps it to the model's reasoning effort. This replaces
6
+ * manual /think toggling with per-prompt automatic selection.
7
+ *
8
+ * Config:
9
+ * { agent: { autoThink: true } }
10
+ *
11
+ * Mechanism:
12
+ * 1. Take the last user message from history
13
+ * 2. Send a minimal classification prompt (expects one-word reply)
14
+ * 3. Map difficulty → reasoningEffort using the model's valid effort enum
15
+ * 4. Set agent.provider.reasoningEffort before the real chat() call
16
+ */
17
+ import { chat } from "./provider/core.mjs"
18
+ import { specForModel } from "./config.mjs"
19
+
20
+ const CLASSIFY_PROMPT = `Classify this coding task's difficulty: low, medium, or high.
21
+
22
+ - low — trivial: rename, typo, formatting, one-liner, direct question
23
+ - medium — localized: small feature, straightforward bug fix, moderate change
24
+ - high — complex: multi-file, debugging, design decisions, large refactor
25
+
26
+ Reply with exactly one word.`
27
+
28
+ const EFFORT_MAP = {
29
+ low: ["low", "minimal", "none", "low"],
30
+ medium: ["high", "medium", "high"],
31
+ high: ["max", "max", "xhigh", "max"],
32
+ }
33
+
34
+ /**
35
+ * Build classifier input from history: the latest real user message (reminders and
36
+ * interrupt injections excluded), plus the previous user message as context when the
37
+ * latest is too short to classify on its own (e.g. "继续" / "还有几个问题").
38
+ * Exported for tests.
39
+ */
40
+ export function buildClassifierInput(history) {
41
+ const isRealUser = (m) =>
42
+ m.role === "user" && typeof m.content === "string"
43
+ && !m.content.startsWith("[System reminder:") && !m.content.startsWith("[User interrupt:")
44
+ const users = history.filter(isRealUser)
45
+ const last = users.at(-1)
46
+ if (!last) return null
47
+ let prompt = last.content
48
+ if (prompt.length < 200 && users.length > 1) {
49
+ prompt = `Previous request (context):\n${users.at(-2).content.slice(0, 1200)}\n\nLatest message:\n${prompt}`
50
+ }
51
+ return prompt.slice(0, 2000)
52
+ }
53
+
54
+ /**
55
+ * Classify the difficulty of the user's prompt and adjust reasoning effort.
56
+ * Only runs on the first turn (turn === 0) of a user message.
57
+ * Returns the resolved level or null if auto-thinking is disabled or classification fails.
58
+ * @param {object} agent
59
+ * @param {number} turn
60
+ * @returns {Promise<string|null>}
61
+ */
62
+ export async function classifyAndApply(agent, turn) {
63
+ if (!agent.config?.agent?.autoThink) return null
64
+ if (turn !== 0) return null // Only classify on the first turn of user input
65
+
66
+ const spec = specForModel(agent.provider.model)
67
+ const validEfforts = spec.reasoningEffortEnum
68
+ if (!validEfforts) return null // Model doesn't support reasoning effort
69
+
70
+ const prompt = buildClassifierInput(agent.history)
71
+ if (prompt == null) return null
72
+
73
+ // Classification call: use same provider, minimal tokens, no tools, no streaming
74
+ let level
75
+ try {
76
+ const classifierProvider = { ...agent.provider, maxTokens: 10 }
77
+ const response = await chat(classifierProvider, {
78
+ messages: [
79
+ { role: "system", content: CLASSIFY_PROMPT },
80
+ { role: "user", content: prompt },
81
+ ],
82
+ tools: [],
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
+ },
98
+ })
99
+ const word = (response.content ?? "").trim().toLowerCase()
100
+ if (word.startsWith("low")) level = "low"
101
+ else if (word.startsWith("medium") || word.startsWith("med")) level = "medium"
102
+ else if (word.startsWith("high")) level = "high"
103
+ else return null // Unparseable
104
+ } catch {
105
+ return null // Classification failure → fall back to current setting
106
+ }
107
+
108
+ // Map difficulty to the closest valid reasoning effort
109
+ const candidates = EFFORT_MAP[level] || EFFORT_MAP.medium
110
+ const matched = candidates.find(e => validEfforts.includes(e))
111
+ if (!matched) return null
112
+
113
+ agent.provider.reasoningEffort = matched
114
+ return matched
115
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * compress-form.mjs — compression request form(CONTEXT-COMPACTION.md §6.14 / D-CC20 ·
3
+ * 批次档 2026-09-18-compression-continuation §2.2):会话续写形态。
4
+ *
5
+ * 压缩请求复用同会话回合请求的消息前缀(同一个 system + 中段「真身」消息),尾部附加指令一条。
6
+ * 消费点(两处同形 · 同一构造):① 压缩 = `context.mjs`(中段切点 = `splitHistory.tailStart`);
7
+ * ② 蒸馏 = `explore-distill.mjs`(切点 = 本 run 末块 `end` → §6.15)——两处均按引用原样携带中段,
8
+ * 均不持声明面(`tools` / `systemPrompt` 由调用点经 extras 随带)。
9
+ *
10
+ * ⚠️ 缓存命中口径(v2 探针复测 2026-09-18 · 批次档 §5):压缩形态**首现复用回合所建前缀**的条件 = 带 tools 声明
11
+ * (**不带** `tool_choice`)且 `reasoning_effort` 同值——8 格 84–98%;不带 tools / 带 `tool_choice:"none"`(该参数使
12
+ * 服务端丢弃 tools 区)/ effort 异值 ⇒ 首现 0%(自建项复跑命中为例外)。v2(设计档 §6.14 备选②)由**调用点**
13
+ * (context.mjs)随带与回合请求同一数组的 `tools`;本模块只构消息序,不持声明面。
14
+ *
15
+ * 纯函数、零 import:指令文本由实参传入(SUMMARIZE_PROMPT 住 context.mjs——不反向 import,免成环)。
16
+ * 中段消息按引用原样携带(不拷贝 / 不截断);切点与摘要段同源(split.tailStart),不新增第二处切割判据。
17
+ */
18
+ export function buildCompressMessages(history, tailStart, systemPrompt, instruction) {
19
+ const messages = []
20
+ if (systemPrompt) messages.push({ role: "system", content: systemPrompt }) // 缺省(undefined)/ 空串 ⇒ 不带头(退化面 1)
21
+ messages.push(...history.slice(0, tailStart))
22
+ messages.push({ role: "user", content: instruction })
23
+ return messages
24
+ }